mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c65da282c8 | ||
|
|
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 | ||
|
|
533fccd1d3 | ||
|
|
ec4bd7600f | ||
|
|
65d4e98d38 | ||
|
|
195b843840 | ||
|
|
00ac66e450 | ||
|
|
351701036b | ||
|
|
2bead5dadd | ||
|
|
dbc707adcd | ||
|
|
5066242ef3 | ||
|
|
e9deff52a7 | ||
|
|
eb3d9b8714 |
63
.github/workflows/check-examples.yml
vendored
63
.github/workflows/check-examples.yml
vendored
@@ -9,66 +9,21 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Get Examples
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
- name: Install JQ Tool
|
||||
uses: mbround18/install-jq@v1
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v examples/README.md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
files: |
|
||||
integrations
|
||||
leptos
|
||||
leptos_config
|
||||
leptos_dom
|
||||
leptos_hot_reload
|
||||
leptos_macro
|
||||
leptos_reactive
|
||||
leptos_server
|
||||
meta
|
||||
router
|
||||
server_fn
|
||||
server_fn_macro
|
||||
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set source_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
matrix-job:
|
||||
test:
|
||||
name: Check
|
||||
needs: [setup]
|
||||
if: needs.setup.outputs.source_changed == 'true'
|
||||
needs: [get-leptos-changed, get-examples-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
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: "check"
|
||||
toolchain: nightly
|
||||
|
||||
87
.github/workflows/check-stable.yml
vendored
87
.github/workflows/check-stable.yml
vendored
@@ -2,82 +2,25 @@ name: Check stable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
files: |
|
||||
integrations
|
||||
leptos
|
||||
leptos_config
|
||||
leptos_dom
|
||||
leptos_hot_reload
|
||||
leptos_macro
|
||||
leptos_reactive
|
||||
leptos_server
|
||||
meta
|
||||
router
|
||||
server_fn
|
||||
server_fn_macro
|
||||
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set source_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
needs: [setup]
|
||||
if: needs.setup.outputs.source_changed == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Check
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
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
|
||||
directory: [examples/counters_stable, examples/counter_without_macros]
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
toolchain: stable
|
||||
|
||||
32
.github/workflows/ci-changed-examples.yml
vendored
Normal file
32
.github/workflows/ci-changed-examples.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: CI Changed Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
get-example-changed:
|
||||
uses: ./.github/workflows/get-example-changed.yml
|
||||
|
||||
get-matrix:
|
||||
needs: [get-example-changed]
|
||||
uses: ./.github/workflows/get-changed-examples-matrix.yml
|
||||
with:
|
||||
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-example-changed, get-matrix]
|
||||
if: needs.get-example-changed.outputs.example_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
@@ -9,45 +9,13 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
files: |
|
||||
integrations
|
||||
leptos
|
||||
leptos_config
|
||||
leptos_dom
|
||||
leptos_hot_reload
|
||||
leptos_macro
|
||||
leptos_reactive
|
||||
leptos_server
|
||||
meta
|
||||
router
|
||||
server_fn
|
||||
server_fn_macro
|
||||
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set source_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
matrix-job:
|
||||
test:
|
||||
name: CI
|
||||
needs: [setup]
|
||||
if: needs.setup.outputs.source_changed == 'true'
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
directory:
|
||||
@@ -73,3 +41,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
name: Verify Changed Examples
|
||||
name: Changed Examples Matrix Call
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_call:
|
||||
inputs:
|
||||
example_changed:
|
||||
description: "Example Changed"
|
||||
required: true
|
||||
type: boolean
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.get-example-changed.outputs.matrix }}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Get Changes
|
||||
get-example-changed:
|
||||
name: Get Changed Example Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
@@ -20,16 +24,6 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get all example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
files: |
|
||||
examples
|
||||
|
||||
- name: List all example files that changed
|
||||
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v36
|
||||
@@ -41,7 +35,7 @@ jobs:
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/Makefile.toml
|
||||
!examples/README.md
|
||||
!examples/*.md
|
||||
json: true
|
||||
quotepath: false
|
||||
|
||||
@@ -51,21 +45,10 @@ jobs:
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
if [ ${{ steps.changed-files.outputs.any_changed }} == 'true' ]; then
|
||||
if [ ${{ inputs.example_changed }} == 'true' ]; then
|
||||
# Create matrix with changed directories
|
||||
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create matrix with one item to prevent an empty vector error
|
||||
echo "matrix={\"directory\":[\"INTERNAL\"]}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
matrix-job:
|
||||
name: Verify
|
||||
needs: [setup]
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "verify-flow"
|
||||
39
.github/workflows/get-example-changed.yml
vendored
Normal file
39
.github/workflows/get-example-changed.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Examples Changed Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
example_changed:
|
||||
description: "Example Changed"
|
||||
value: ${{ jobs.get-example-changed.outputs.example_changed }}
|
||||
|
||||
jobs:
|
||||
get-example-changed:
|
||||
name: Get Example Changed
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
files: |
|
||||
examples
|
||||
!examples/cargo-make
|
||||
!examples/gtk
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
|
||||
- name: List example files that changed
|
||||
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set example_changed
|
||||
id: set-example-changed
|
||||
run: |
|
||||
echo "example_changed=${{ steps.changed-files.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
40
.github/workflows/get-examples-matrix.yml
vendored
Normal file
40
.github/workflows/get-examples-matrix.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Get Examples Matrix Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.create.outputs.matrix }}
|
||||
|
||||
jobs:
|
||||
create:
|
||||
name: Create Examples Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install JQ Tool
|
||||
uses: mbround18/install-jq@v1
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v .md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
pwd
|
||||
ls | sort -u
|
||||
44
.github/workflows/get-leptos-changed.yml
vendored
Normal file
44
.github/workflows/get-leptos-changed.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Get Leptos Changed Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
leptos_changed:
|
||||
description: "Leptos Changed"
|
||||
value: ${{ jobs.create.outputs.leptos_changed }}
|
||||
|
||||
jobs:
|
||||
create:
|
||||
name: Detect Source Change
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v36
|
||||
with:
|
||||
files: |
|
||||
integrations
|
||||
leptos
|
||||
leptos_config
|
||||
leptos_dom
|
||||
leptos_hot_reload
|
||||
leptos_macro
|
||||
leptos_reactive
|
||||
leptos_server
|
||||
meta
|
||||
router
|
||||
server_fn
|
||||
server_fn_macro
|
||||
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set leptos_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
echo "leptos_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
52
.github/workflows/publish-book.yml
vendored
52
.github/workflows/publish-book.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Deploy book
|
||||
on:
|
||||
push:
|
||||
paths: ['docs/book/**']
|
||||
paths: ["docs/book/**"]
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -9,29 +9,29 @@ 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@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
|
||||
|
||||
62
.github/workflows/run-cargo-make-task.yml
vendored
62
.github/workflows/run-cargo-make-task.yml
vendored
@@ -9,34 +9,27 @@ on:
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
toolchain:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
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
|
||||
- name: Install playwright browser dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
@@ -84,17 +77,38 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install Chrome Webriver
|
||||
- name: Maybe install chromedriver
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install chromium-chromedriver
|
||||
project_makefile=${{inputs.directory}}/Makefile.toml
|
||||
webdriver_count=$(cat $project_makefile | grep "cargo-make/webdriver.toml" | wc -l)
|
||||
if [ $webdriver_count -eq 1 ]; then
|
||||
if ! command -v chromedriver &>/dev/null; then
|
||||
echo chromedriver required
|
||||
sudo apt-get update
|
||||
sudo apt-get install chromium-chromedriver
|
||||
else
|
||||
echo chromedriver is already installed
|
||||
fi
|
||||
else
|
||||
echo chromedriver is not required
|
||||
fi
|
||||
|
||||
- name: Maybe install playwright browser dependencies
|
||||
run: |
|
||||
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 }}
|
||||
run: |
|
||||
if [ "${{ inputs.directory }}" = "INTERNAL" ]; then
|
||||
echo No verification required
|
||||
else
|
||||
cd ${{ inputs.directory }}
|
||||
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
|
||||
fi
|
||||
cd ${{ inputs.directory }}
|
||||
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
|
||||
|
||||
39
.github/workflows/verify-all-examples.yml
vendored
39
.github/workflows/verify-all-examples.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Verify All Examples
|
||||
name: CI Examples
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -10,38 +10,17 @@ on:
|
||||
- cron: "0 8 * * *"
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Get Examples
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
|
||||
- name: Install JQ Tool
|
||||
uses: mbround18/install-jq@v1
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v examples/README.md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
matrix-job:
|
||||
name: Verify
|
||||
needs: [setup]
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-examples-matrix]
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
|
||||
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: "verify-flow"
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
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.4.8"
|
||||
version = "0.5.0-rc2"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.4.8" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.8" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.8" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.8" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.8" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.8" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.8" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.8" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.8" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.8" }
|
||||
leptos_router = { path = "./router", version = "0.4.8" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.8" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.8" }
|
||||
leptos = { path = "./leptos", version = "0.5.0-rc2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0-rc2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-rc2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0-rc2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-rc2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0-rc2" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0-rc2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-rc2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-rc2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0-rc2" }
|
||||
leptos_router = { path = "./router", version = "0.5.0-rc2" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0-rc2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-rc2" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
30
README.md
30
README.md
@@ -16,9 +16,9 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
// create a reactive signal with the initial value
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
|
||||
// create event handlers for our buttons
|
||||
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
@@ -27,23 +27,29 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// create user interfaces with the declarative `view!` macro
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
<button on:click=clear>Clear</button>
|
||||
<button on:click=decrement>-1</button>
|
||||
// text nodes can be quoted or unquoted
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=increment>"+1"</button>
|
||||
<button on:click=increment>+1</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
|
||||
pub fn main() {
|
||||
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 /> })
|
||||
mount_to_body(|| view! {
|
||||
<SimpleCounter initial_value=3 />
|
||||
})
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 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.
|
||||
@@ -113,7 +119,7 @@ People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
The APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
|
||||
The APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases, in terms of architecture.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
@@ -152,13 +158,13 @@ There are some practical differences that make a significant difference:
|
||||
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0); // a signal
|
||||
let (count, set_count) = create_signal(0); // a signal
|
||||
let double_count = move || count() * 2; // a derived signal
|
||||
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
|
||||
let memoized_count = create_memo(move |_| count() * 3); // a memo
|
||||
// all are accessed by calling them
|
||||
assert_eq!(count(), 0);
|
||||
assert_eq!(double_count(), 0);
|
||||
|
||||
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.
|
||||
@@ -5,9 +5,13 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", features = ["ssr"] }
|
||||
leptos = { path = "../leptos", features = [
|
||||
"ssr",
|
||||
"nightly",
|
||||
"experimental-islands",
|
||||
] }
|
||||
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"
|
||||
|
||||
@@ -7,15 +7,15 @@ fn leptos_deep_creation(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
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(cx, move |_| prev.get() + 1));
|
||||
memos.push(create_memo(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -31,14 +31,14 @@ fn leptos_deep_update(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
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(cx, move |_| prev.get() + 1));
|
||||
memos.push(create_memo(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
@@ -56,12 +56,11 @@ fn leptos_narrowing_down(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
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(cx, move |_| {
|
||||
let memo = create_memo(move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
@@ -78,10 +77,10 @@ fn leptos_fanning_out(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
create_scope(runtime, || {
|
||||
let sig = create_rw_signal(0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.map(|_| create_memo(move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
@@ -99,17 +98,16 @@ fn leptos_narrowing_update(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
create_scope(runtime, || {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
let memo = create_memo(move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
create_isomorphic_effect({
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
@@ -141,9 +139,9 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
.map(|_| {
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move || {
|
||||
let (r, w) = create_signal(0);
|
||||
create_isomorphic_effect({
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
}
|
||||
@@ -163,7 +161,9 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
|
||||
#[bench]
|
||||
fn rs_deep_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
|
||||
};
|
||||
|
||||
let sc = ClientRuntime::new_root_scope();
|
||||
b.iter(|| {
|
||||
@@ -184,7 +184,9 @@ fn rs_deep_update(b: &mut Bencher) {
|
||||
|
||||
#[bench]
|
||||
fn rs_fanning_out(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
use reactive_signals::{
|
||||
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
|
||||
};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -200,18 +202,17 @@ fn rs_fanning_out(b: &mut Bencher) {
|
||||
|
||||
#[bench]
|
||||
fn rs_narrowing_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
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 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>()
|
||||
}
|
||||
move || sigs.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
signal!(cx, {
|
||||
|
||||
@@ -7,7 +7,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
leptos_dom::HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
|
||||
fn Counter(initial: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial);
|
||||
view! {
|
||||
cx,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub use leptos::*;
|
||||
use miniserde::*;
|
||||
use web_sys::HtmlInputElement;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
@@ -9,13 +9,13 @@ pub struct Todos(pub Vec<Todo>);
|
||||
const STORAGE_KEY: &str = "todos-leptos";
|
||||
|
||||
impl Todos {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub fn new_with_1000(cx: Scope) -> Self {
|
||||
pub fn new_with_1000() -> Self {
|
||||
let todos = (0..1000)
|
||||
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
|
||||
.map(|id| Todo::new(id, format!("Todo #{id}")))
|
||||
.collect();
|
||||
Self(todos)
|
||||
}
|
||||
@@ -72,13 +72,17 @@ pub struct Todo {
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(cx: Scope, id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(cx, id, title, false)
|
||||
pub fn new(id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
|
||||
let (title, set_title) = create_signal(cx, title);
|
||||
let (completed, set_completed) = create_signal(cx, completed);
|
||||
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,
|
||||
@@ -98,7 +102,7 @@ const ESCAPE_KEY: u32 = 27;
|
||||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
#[component]
|
||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
@@ -107,10 +111,10 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
.map(|last| last + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let (todos, set_todos) = create_signal(cx, todos);
|
||||
provide_context(cx, set_todos);
|
||||
let (todos, set_todos) = create_signal(todos);
|
||||
provide_context(set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
let (mode, set_mode) = create_signal(Mode::All);
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
@@ -120,7 +124,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, next_id, title.to_string());
|
||||
let new = Todo::new(next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
@@ -128,7 +132,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
@@ -148,7 +152,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
|
||||
// effect to serialize to JSON
|
||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
@@ -163,7 +167,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
@@ -188,8 +192,8 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |cx, todo: Todo| {
|
||||
view! { cx, <Todo todo=todo.clone()/> }
|
||||
view=move |todo: Todo| {
|
||||
view! { <Todo todo=todo.clone()/> }
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
@@ -236,14 +240,14 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
}.into_view()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
//let input = NodeRef::new(cx);
|
||||
pub fn Todo(todo: Todo) -> impl IntoView {
|
||||
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();
|
||||
@@ -255,7 +259,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
|
||||
@@ -268,7 +272,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! { cx,
|
||||
view! {
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
@@ -319,8 +323,8 @@ pub struct TodoSerialized {
|
||||
}
|
||||
|
||||
impl TodoSerialized {
|
||||
pub fn into_todo(self, cx: Scope) -> Todo {
|
||||
Todo::new_with_completed(cx, self.id, self.title, self.completed)
|
||||
pub fn into_todo(self, ) -> Todo {
|
||||
Todo::new_with_completed(self.id, self.title, self.completed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! { <TodoMVC todos=Todos::new()/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
|
||||
@@ -9,10 +9,10 @@ This document is intended as a running list of common issues, with example code
|
||||
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, false);
|
||||
let (a, set_a) = create_signal(0);
|
||||
let (b, set_b) = create_signal(false);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
if a() > 5 {
|
||||
set_b(true);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
|
||||
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (a, set_a) = create_signal(0);
|
||||
let b = move || a () > 5;
|
||||
```
|
||||
|
||||
@@ -34,19 +34,19 @@ Sometimes you have nested signals: for example, hash-map that can change over ti
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let resources = create_rw_signal(cx, HashMap::new());
|
||||
pub fn App() -> impl IntoView {
|
||||
let resources = create_rw_signal(HashMap::new());
|
||||
|
||||
let update = move |id: usize| {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(cx, 0))
|
||||
.or_insert_with(|| create_rw_signal(0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
|
||||
<button on:click=move |_| update(1)>"+"</button>
|
||||
@@ -55,17 +55,17 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
Clicking the button twice will cause a panic, because of the nested signal *read*. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
|
||||
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
|
||||
|
||||
You can fix this fairly easily by using the [`Scope::batch()`](https://docs.rs/leptos/latest/leptos/struct.Scope.html#method.batch) method:
|
||||
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
|
||||
|
||||
```rust
|
||||
let update = move |id: usize| {
|
||||
cx.batch(move || {
|
||||
batch(move || {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(cx, 0))
|
||||
.or_insert_with(|| create_rw_signal(0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
});
|
||||
@@ -83,11 +83,11 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
|
||||
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
// ❌ reactivity doesn't work as expected: typing only updates the default
|
||||
// of each input, so if you start typing in the second input, it won't
|
||||
// update the first one
|
||||
@@ -97,11 +97,11 @@ view! {
|
||||
```
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
// ✅ works as intended by setting the value *property*
|
||||
<input prop:value=a on:input=on_input />
|
||||
<input prop:value=a on:input=on_input />
|
||||
|
||||
@@ -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 upcoming `0.5.0` release, which you can install as version `0.5.0-rc2`. 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.
|
||||
|
||||
@@ -23,12 +23,15 @@ cargo init leptos-tutorial
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
cargo add leptos --features=csr,nightly
|
||||
cargo add leptos@0.5.0-rc2 --features=csr,nightly
|
||||
```
|
||||
|
||||
> **Note**: This version of the book reflects the upcoming Leptos 0.5.0 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
|
||||
cargo add leptos --features=csr
|
||||
cargo add leptos@0.5.0-rc2 --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.
|
||||
@@ -64,7 +67,7 @@ And add a simple “Hello, world!” to your `main.rs`
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
mount_to_body(|| view! { <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -29,15 +29,15 @@ all its children and descendants using `provide_context`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
fn App() -> impl IntoView {
|
||||
// here we create a signal in the root that can be consumed
|
||||
// anywhere in the app.
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
// we'll pass the setter to specific components,
|
||||
// but provide the count itself to the whole app via context
|
||||
provide_context(cx, count);
|
||||
provide_context(count);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
// SetterButton is allowed to modify the count
|
||||
<SetterButton set_count/>
|
||||
// These consumers can only read from it
|
||||
@@ -57,14 +57,14 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
```rust
|
||||
/// A component that does some "fancy" math with the global count
|
||||
#[component]
|
||||
fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
fn FancyMath() -> impl IntoView {
|
||||
// here we consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>(cx)
|
||||
let count = use_context::<ReadSignal<u32>>()
|
||||
// we know we just provided this in the parent component
|
||||
.expect("there to be a `count` signal provided");
|
||||
let is_even = move || count() & 1 == 0;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="consumer blue">
|
||||
"The number "
|
||||
<strong>{count}</strong>
|
||||
@@ -89,17 +89,17 @@ struct GlobalState {
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
count: create_rw_signal(cx, 0),
|
||||
name: create_rw_signal(cx, "Bob".to_string())
|
||||
count: create_rw_signal(0),
|
||||
name: create_rw_signal("Bob".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
provide_context(cx, GlobalState::new(cx));
|
||||
fn App() -> impl IntoView {
|
||||
provide_context(GlobalState::new());
|
||||
|
||||
// etc.
|
||||
}
|
||||
@@ -117,8 +117,8 @@ struct GlobalState {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
provide_context(cx, create_rw_signal(GlobalState::default()));
|
||||
fn App() -> impl IntoView {
|
||||
provide_context(create_rw_signal(GlobalState::default()));
|
||||
|
||||
// etc.
|
||||
}
|
||||
@@ -127,8 +127,8 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
But there’s a problem: because our whole state is wrapped in one signal, updating the value of one field will cause reactive updates in parts of the UI that only depend on the other.
|
||||
|
||||
```rust
|
||||
let state = expect_context::<RwSignal<GlobalState>>(cx);
|
||||
view! { cx,
|
||||
let state = expect_context::<RwSignal<GlobalState>>();
|
||||
view! {
|
||||
<button on:click=move |_| state.update(|n| *n += 1)>"+1"</button>
|
||||
<p>{move || state.with(|state| state.name.clone())}</p>
|
||||
}
|
||||
@@ -143,12 +143,12 @@ Here, instead of reading from the state signal directly, we create “slices”
|
||||
```rust
|
||||
/// A component that updates the count in the global state.
|
||||
#[component]
|
||||
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
let state = expect_context::<RwSignal<GlobalState>>(cx);
|
||||
fn GlobalStateCounter() -> impl IntoView {
|
||||
let state = expect_context::<RwSignal<GlobalState>>();
|
||||
|
||||
// `create_slice` lets us create a "lens" into the data
|
||||
let (count, set_count) = create_slice(
|
||||
cx,
|
||||
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
@@ -157,7 +157,7 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
|state, n| state.count = n,
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="consumer blue">
|
||||
<button
|
||||
on:click=move |_| {
|
||||
@@ -214,15 +214,15 @@ use leptos::*;
|
||||
// components using provide_context(). Changing it will only cause rerendering
|
||||
// in the specific places it is actually used, not the whole app.
|
||||
#[component]
|
||||
fn Option2(cx: Scope) -> impl IntoView {
|
||||
fn Option2() -> impl IntoView {
|
||||
// here we create a signal in the root that can be consumed
|
||||
// anywhere in the app.
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
// we'll pass the setter to specific components,
|
||||
// but provide the count itself to the whole app via context
|
||||
provide_context(cx, count);
|
||||
provide_context(count);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"Option 2: Passing Signals"</h1>
|
||||
// SetterButton is allowed to modify the count
|
||||
<SetterButton set_count/>
|
||||
@@ -237,8 +237,8 @@ fn Option2(cx: Scope) -> impl IntoView {
|
||||
|
||||
/// A button that increments our global counter.
|
||||
#[component]
|
||||
fn SetterButton(cx: Scope, set_count: WriteSignal<u32>) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn SetterButton(set_count: WriteSignal<u32>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="provider red">
|
||||
<button on:click=move |_| set_count.update(|count| *count += 1)>
|
||||
"Increment Global Count"
|
||||
@@ -249,14 +249,14 @@ fn SetterButton(cx: Scope, set_count: WriteSignal<u32>) -> impl IntoView {
|
||||
|
||||
/// A component that does some "fancy" math with the global count
|
||||
#[component]
|
||||
fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
fn FancyMath() -> impl IntoView {
|
||||
// here we consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>(cx)
|
||||
let count = use_context::<ReadSignal<u32>>()
|
||||
// we know we just provided this in the parent component
|
||||
.expect("there to be a `count` signal provided");
|
||||
let is_even = move || count() & 1 == 0;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="consumer blue">
|
||||
"The number "
|
||||
<strong>{count}</strong>
|
||||
@@ -272,17 +272,17 @@ fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
|
||||
/// A component that shows a list of items generated from the global count.
|
||||
#[component]
|
||||
fn ListItems(cx: Scope) -> impl IntoView {
|
||||
fn ListItems() -> impl IntoView {
|
||||
// again, consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>(cx).expect("there to be a `count` signal provided");
|
||||
let count = use_context::<ReadSignal<u32>>().expect("there to be a `count` signal provided");
|
||||
|
||||
let squares = move || {
|
||||
(0..count())
|
||||
.map(|n| view! { cx, <li>{n}<sup>"2"</sup> " is " {n * n}</li> })
|
||||
.map(|n| view! { <li>{n}<sup>"2"</sup> " is " {n * n}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="consumer green">
|
||||
<ul>{squares}</ul>
|
||||
</div>
|
||||
@@ -304,13 +304,13 @@ struct GlobalState {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Option3(cx: Scope) -> impl IntoView {
|
||||
fn Option3() -> impl IntoView {
|
||||
// we'll provide a single signal that holds the whole state
|
||||
// each component will be responsible for creating its own "lens" into it
|
||||
let state = create_rw_signal(cx, GlobalState::default());
|
||||
provide_context(cx, state);
|
||||
let state = create_rw_signal(GlobalState::default());
|
||||
provide_context(state);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"Option 3: Passing Signals"</h1>
|
||||
<div class="red consumer" style="width: 100%">
|
||||
<h2>"Current Global State"</h2>
|
||||
@@ -329,12 +329,12 @@ fn Option3(cx: Scope) -> impl IntoView {
|
||||
|
||||
/// A component that updates the count in the global state.
|
||||
#[component]
|
||||
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
let state = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
|
||||
fn GlobalStateCounter() -> impl IntoView {
|
||||
let state = use_context::<RwSignal<GlobalState>>().expect("state to have been provided");
|
||||
|
||||
// `create_slice` lets us create a "lens" into the data
|
||||
let (count, set_count) = create_slice(
|
||||
cx,
|
||||
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
@@ -343,7 +343,7 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
|state, n| state.count = n,
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="consumer blue">
|
||||
<button
|
||||
on:click=move |_| {
|
||||
@@ -360,14 +360,13 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
|
||||
/// A component that updates the count in the global state.
|
||||
#[component]
|
||||
fn GlobalStateInput(cx: Scope) -> impl IntoView {
|
||||
let state = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
|
||||
fn GlobalStateInput() -> impl IntoView {
|
||||
let state = use_context::<RwSignal<GlobalState>>().expect("state to have been provided");
|
||||
|
||||
// this slice is completely independent of the `count` slice
|
||||
// that we created in the other component
|
||||
// neither of them will cause the other to rerun
|
||||
let (name, set_name) = create_slice(
|
||||
cx,
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
@@ -376,7 +375,7 @@ fn GlobalStateInput(cx: Scope) -> impl IntoView {
|
||||
|state, n| state.name = n,
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="consumer green">
|
||||
<input
|
||||
type="text"
|
||||
@@ -395,7 +394,7 @@ fn GlobalStateInput(cx: Scope) -> 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(|cx| view! { cx, <Option2/><Option3/> })
|
||||
leptos::mount_to_body(|| view! { <Option2/><Option3/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- [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)
|
||||
@@ -46,3 +47,4 @@
|
||||
- [Deployment](./deployment.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)
|
||||
|
||||
49
docs/book/src/appendix_dx.md
Normal file
49
docs/book/src/appendix_dx.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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": [
|
||||
"server",
|
||||
"component"
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
neovim with lspconfig:
|
||||
|
||||
```lua
|
||||
require('lspconfig').rust_analyzer.setup {
|
||||
-- Other Configs ...
|
||||
settings = {
|
||||
["rust-analyzer"] = {
|
||||
-- Other Settings ...
|
||||
procMacro = {
|
||||
ignored = {
|
||||
leptos_macro = {
|
||||
"server",
|
||||
"component",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Helix, in `.helix/languages.toml`:
|
||||
|
||||
```toml
|
||||
[[language]]
|
||||
name = "rust"
|
||||
config = { procMacro = {ignored = {leptos_macro = ["component"]}}}
|
||||
```
|
||||
@@ -38,13 +38,13 @@ So imagine the following code:
|
||||
|
||||
```rust
|
||||
// A
|
||||
let (name, set_name) = create_signal(cx, "Alice");
|
||||
let (name, set_name) = create_signal("Alice");
|
||||
|
||||
// B
|
||||
let name_upper = create_memo(cx, move |_| name.with(|n| n.to_uppercase()));
|
||||
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
|
||||
|
||||
// C
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
log!("{}", name_upper());
|
||||
});
|
||||
|
||||
@@ -67,21 +67,21 @@ Let’s make it a little more complex.
|
||||
|
||||
```rust
|
||||
// A
|
||||
let (name, set_name) = create_signal(cx, "Alice");
|
||||
let (name, set_name) = create_signal("Alice");
|
||||
|
||||
// B
|
||||
let name_upper = create_memo(cx, move |_| name.with(|n| n.to_uppercase()));
|
||||
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
|
||||
|
||||
// C
|
||||
let name_len = create_memo(cx, move |_| name.len());
|
||||
let name_len = create_memo(move |_| name.len());
|
||||
|
||||
// D
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
log!("len = {}", name_len());
|
||||
});
|
||||
|
||||
// E
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
log!("name = {}", name_upper());
|
||||
});
|
||||
```
|
||||
@@ -131,16 +131,16 @@ One more example, of what’s sometimes called **the diamond problem**.
|
||||
|
||||
```rust
|
||||
// A
|
||||
let (name, set_name) = create_signal(cx, "Alice");
|
||||
let (name, set_name) = create_signal("Alice");
|
||||
|
||||
// B
|
||||
let name_upper = create_memo(cx, move |_| name.with(|n| n.to_uppercase()));
|
||||
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
|
||||
|
||||
// C
|
||||
let name_len = create_memo(cx, move |_| name.len());
|
||||
let name_len = create_memo(move |_| name.len());
|
||||
|
||||
// D
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
log!("{} is {} characters long", name_upper(), name_len());
|
||||
});
|
||||
```
|
||||
@@ -218,7 +218,7 @@ All of this is cool, and memos are pretty great. But most actual applications ha
|
||||
In cases in which the computation itself is cheaper than this reactive work, you should avoid “over-wrapping” with memos and simply use derived signals. Here’s a great example in which you should never use a memo:
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 1);
|
||||
let (a, set_a) = create_signal(1);
|
||||
// none of these make sense as memos
|
||||
let b = move || a() + 2;
|
||||
let c = move || b() % 2 == 0;
|
||||
@@ -234,10 +234,10 @@ Even though memoizing would technically save an extra calculation of `d` between
|
||||
At the very most, you might consider memoizing the final node before running some expensive side effect:
|
||||
|
||||
```rust
|
||||
let text = create_memo(cx, move |_| {
|
||||
d()
|
||||
let text = create_memo(move |_| {
|
||||
d()
|
||||
});
|
||||
create_effect(cx, move |_| {
|
||||
engrave_text_into_bar_of_gold(&text());
|
||||
create_effect(move |_| {
|
||||
engrave_text_into_bar_of_gold(&text());
|
||||
});
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if it’s still pending.
|
||||
|
||||
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments (other than the ubiquitous `cx`):
|
||||
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments:
|
||||
|
||||
1. a source signal, which will generate a new `Future` whenever it changes
|
||||
2. a fetcher function, which takes the data from that signal and returns a `Future`
|
||||
@@ -11,14 +11,14 @@ Here’s an example
|
||||
|
||||
```rust
|
||||
// our source signal: some synchronous, local state
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// our resource
|
||||
let async_data = create_resource(cx,
|
||||
let async_data = create_resource(
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
logging::log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
@@ -27,23 +27,20 @@ let async_data = create_resource(cx,
|
||||
To create a resource that simply runs once, you can pass a non-reactive, empty source signal:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
let once = create_resource(|| (), |_| async move { load_data().await });
|
||||
```
|
||||
|
||||
To access the value you can use `.read(cx)` or `.with(cx, |data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but with two differences
|
||||
|
||||
1. For any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
2. They take a `Scope` argument. You’ll see why in the next chapter, on `<Suspense/>`.
|
||||
To access the value you can use `.read()` or `.with(|data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but for any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
|
||||
So, you can show the current state of a resource in your view:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
view! { cx,
|
||||
let once = create_resource(|| (), |_| async move { load_data().await });
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
{move || match once.read() {
|
||||
None => view! { <p>"Loading..."</p> }.into_view(),
|
||||
Some(data) => view! { <ShowData data/> }.into_view()
|
||||
}}
|
||||
}
|
||||
```
|
||||
@@ -71,13 +68,13 @@ async fn load_data(value: i32) -> i32 {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
fn App() -> impl IntoView {
|
||||
// this count is our synchronous, local state
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// create_resource takes two arguments after its scope
|
||||
let async_data = create_resource(
|
||||
cx,
|
||||
|
||||
// the first is the "source signal"
|
||||
count,
|
||||
// the second is the loader
|
||||
@@ -90,14 +87,14 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
// you can also create resources that only load once
|
||||
// just return the unit type () from the source signal
|
||||
// that doesn't depend on anything: we just load it once
|
||||
let stable = create_resource(cx, || (), |_| async move { load_data(1).await });
|
||||
let stable = create_resource(|| (), |_| async move { load_data(1).await });
|
||||
|
||||
// we can access the resource values with .read()
|
||||
// this will reactively return None before the Future has resolved
|
||||
// and update to Some(T) when it has resolved
|
||||
let async_result = move || {
|
||||
async_data
|
||||
.read(cx)
|
||||
.read()
|
||||
.map(|value| format!("Server returned {value:?}"))
|
||||
// This loading state will only show before the first load
|
||||
.unwrap_or_else(|| "Loading...".into())
|
||||
@@ -108,7 +105,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
let loading = async_data.loading();
|
||||
let is_loading = move || if loading() { "Loading..." } else { "Idle." };
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
@@ -117,7 +114,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
"Click me"
|
||||
</button>
|
||||
<p>
|
||||
<code>"stable"</code>": " {move || stable.read(cx)}
|
||||
<code>"stable"</code>": " {move || stable.read()}
|
||||
</p>
|
||||
<p>
|
||||
<code>"count"</code>": " {count}
|
||||
@@ -132,7 +129,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let (count, set_count) = create_signal(0);
|
||||
let once = create_resource(count, |count| async move { load_a(count).await });
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
{move || match once.read() {
|
||||
None => view! { <p>"Loading..."</p> }.into_view(),
|
||||
Some(data) => view! { <ShowData data/> }.into_view()
|
||||
}}
|
||||
}
|
||||
```
|
||||
@@ -18,19 +18,19 @@ view! { cx,
|
||||
But what if we have two resources, and want to wait for both of them?
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
let (count, set_count) = create_signal(0);
|
||||
let (count2, set_count2) = create_signal(0);
|
||||
let a = create_resource(count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
{move || match (a.read(), b.read()) {
|
||||
(Some(a), Some(b)) => view! {
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx),
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
|
||||
}.into_view(),
|
||||
_ => view! { <p>"Loading..."</p> }.into_view()
|
||||
}}
|
||||
}
|
||||
```
|
||||
@@ -40,26 +40,26 @@ That’s not _so_ bad, but it’s kind of annoying. What if we could invert the
|
||||
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If it’s still waiting for resources to load, it shows the `fallback`. When they’ve all loaded, it shows the children.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
let (count, set_count) = create_signal(0);
|
||||
let (count2, set_count2) = create_signal(0);
|
||||
let a = create_resource(count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"My Data"</h1>
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read(cx)
|
||||
.map(|a| view! { cx, <ShowA a/> })
|
||||
a.read()
|
||||
.map(|a| view! { <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read(cx)
|
||||
.map(|b| view! { cx, <ShowB b/> })
|
||||
b.read()
|
||||
.map(|b| view! { <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
@@ -84,10 +84,10 @@ async fn fetch_monkeys(monkey: i32) -> i32 {
|
||||
// maybe this didn't need to be async
|
||||
monkey * 2
|
||||
}
|
||||
view! { cx,
|
||||
view! {
|
||||
<Await
|
||||
// `future` provides the `Future` to be resolved
|
||||
future=|cx| fetch_monkeys(3)
|
||||
future=|| fetch_monkeys(3)
|
||||
// the data is bound to whatever variable name you provide
|
||||
bind:data
|
||||
>
|
||||
@@ -114,17 +114,17 @@ async fn important_api_call(name: String) -> String {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (name, set_name) = create_signal(cx, "Bill".to_string());
|
||||
fn App() -> impl IntoView {
|
||||
let (name, set_name) = create_signal("Bill".to_string());
|
||||
|
||||
// this will reload every time `name` changes
|
||||
let async_data = create_resource(
|
||||
cx,
|
||||
|
||||
name,
|
||||
|name| async move { important_api_call(name).await },
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<input
|
||||
on:input=move |ev| {
|
||||
set_name(event_target_value(&ev));
|
||||
@@ -135,20 +135,20 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<Suspense
|
||||
// the fallback will show whenever a resource
|
||||
// read "under" the suspense is loading
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
>
|
||||
// the children will be rendered once initially,
|
||||
// and then whenever any resources has been resolved
|
||||
<p>
|
||||
"Your shouting name is "
|
||||
{move || async_data.read(cx)}
|
||||
{move || async_data.read()}
|
||||
</p>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -29,13 +29,13 @@ async fn important_api_call(id: usize) -> String {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (tab, set_tab) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (tab, set_tab) = create_signal(0);
|
||||
|
||||
// this will reload every time `tab` changes
|
||||
let user_data = create_resource(cx, tab, |tab| async move { important_api_call(tab).await });
|
||||
let user_data = create_resource(tab, |tab| async move { important_api_call(tab).await });
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="buttons">
|
||||
<button
|
||||
on:click=move |_| set_tab(0)
|
||||
@@ -65,17 +65,17 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
// the fallback will show initially
|
||||
// on subsequent reloads, the current child will
|
||||
// continue showing
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
>
|
||||
<p>
|
||||
{move || user_data.read(cx)}
|
||||
{move || user_data.read()}
|
||||
</p>
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -16,22 +16,22 @@ async fn add_todo_request(new_title: &str) -> Uuid {
|
||||
}
|
||||
```
|
||||
|
||||
`create_action` takes a reactive `Scope` and an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
|
||||
`create_action` takes an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
|
||||
|
||||
> The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
|
||||
>
|
||||
> ```rust
|
||||
> // if there's a single argument, just use that
|
||||
> let action1 = create_action(cx, |input: &String| {
|
||||
> let action1 = create_action(|input: &String| {
|
||||
> let input = input.clone();
|
||||
> async move { todo!() }
|
||||
> });
|
||||
>
|
||||
> // if there are no arguments, use the unit type `()`
|
||||
> let action2 = create_action(cx, |input: &()| async { todo!() });
|
||||
> let action2 = create_action(|input: &()| async { todo!() });
|
||||
>
|
||||
> // if there are multiple arguments, use a tuple
|
||||
> let action3 = create_action(cx,
|
||||
> let action3 = create_action(
|
||||
> |input: &(usize, String)| async { todo!() }
|
||||
> );
|
||||
> ```
|
||||
@@ -41,7 +41,7 @@ async fn add_todo_request(new_title: &str) -> Uuid {
|
||||
So in this case, all we need to do to create an action is
|
||||
|
||||
```rust
|
||||
let add_todo_action = create_action(cx, |input: &String| {
|
||||
let add_todo_action = create_action(|input: &String| {
|
||||
let input = input.to_owned();
|
||||
async move { add_todo_request(&input).await }
|
||||
});
|
||||
@@ -66,9 +66,9 @@ let todo_id = add_todo_action.value(); // RwSignal<Option<Uuid>>
|
||||
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
|
||||
|
||||
```rust
|
||||
let input_ref = create_node_ref::<Input>(cx);
|
||||
let input_ref = create_node_ref::<Input>();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<form
|
||||
on:submit=move |ev| {
|
||||
ev.prevent_default(); // don't reload the page...
|
||||
@@ -116,10 +116,10 @@ async fn add_todo(text: &str) -> Uuid {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
fn App() -> impl IntoView {
|
||||
// an action takes an async function with single argument
|
||||
// it can be a simple type, a struct, or ()
|
||||
let add_todo = create_action(cx, |input: &String| {
|
||||
let add_todo = create_action(|input: &String| {
|
||||
// the input is a reference, but we need the Future to own it
|
||||
// this is important: we need to clone and move into the Future
|
||||
// so it has a 'static lifetime
|
||||
@@ -133,9 +133,9 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
let pending = add_todo.pending();
|
||||
let todo_id = add_todo.value();
|
||||
|
||||
let input_ref = create_node_ref::<Input>(cx);
|
||||
let input_ref = create_node_ref::<Input>();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<form
|
||||
on:submit=move |ev| {
|
||||
ev.prevent_default(); // don't reload the page...
|
||||
@@ -168,7 +168,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -54,7 +54,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/server/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
|
||||
@@ -68,7 +68,7 @@ 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).
|
||||
|
||||
@@ -7,12 +7,12 @@ As you build components you may occasionally find yourself wanting to “project
|
||||
Consider the following:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
@@ -22,7 +22,7 @@ where
|
||||
when=move || todo!()
|
||||
fallback=fallback
|
||||
>
|
||||
{children(cx)}
|
||||
{children()}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
@@ -50,18 +50,16 @@ If you want to really understand the issue here, it may help to look at the expa
|
||||
|
||||
```rust
|
||||
Suspense(
|
||||
cx,
|
||||
::leptos::component_props_builder(&Suspense)
|
||||
.fallback(|| ())
|
||||
.children({
|
||||
// fallback and children are moved into this closure
|
||||
Box::new(move |cx| {
|
||||
Box::new(move || {
|
||||
{
|
||||
// fallback and children captured here
|
||||
leptos::Fragment::lazy(|| {
|
||||
vec![
|
||||
(Show(
|
||||
cx,
|
||||
::leptos::component_props_builder(&Show)
|
||||
.when(|| true)
|
||||
// but fallback is moved into Show here
|
||||
@@ -70,7 +68,7 @@ Suspense(
|
||||
.children(children)
|
||||
.build(),
|
||||
)
|
||||
.into_view(cx)),
|
||||
.into_view()),
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -91,22 +89,22 @@ We can solve this problem by using the [`store_value`](https://docs.rs/leptos/la
|
||||
In this case, it’s really simple:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let fallback = store_value(cx, fallback);
|
||||
let children = store_value(cx, children);
|
||||
view! { cx,
|
||||
let fallback = store_value(fallback);
|
||||
let children = store_value(children);
|
||||
view! {
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
<Show
|
||||
when=|| todo!()
|
||||
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
|
||||
fallback=move || fallback.with_value(|fallback| fallback())
|
||||
>
|
||||
{children.with_value(|children| children(cx))}
|
||||
{children.with_value(|children| children())}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
@@ -125,9 +123,9 @@ Consider this example
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
let name = "Alice".to_string();
|
||||
view! { cx,
|
||||
view! {
|
||||
<Outer>
|
||||
<Inner>
|
||||
<Inmost name=name.clone()/>
|
||||
@@ -137,18 +135,18 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
pub fn Outer(ChildrenFn) -> impl IntoView {
|
||||
children()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
pub fn Inner(ChildrenFn) -> impl IntoView {
|
||||
children()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Inmost(ng) -> impl IntoView {
|
||||
view! {
|
||||
<p>{name}</p>
|
||||
}
|
||||
}
|
||||
@@ -165,7 +163,7 @@ It’s captured through multiple levels of children that need to run more than o
|
||||
In this case, the `clone:` syntax comes in handy. Calling `clone:name` will clone `name` _before_ moving it into `<Inner/>`’s children, which solves our ownership issue.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<Outer>
|
||||
<Inner clone:name>
|
||||
<Inmost name=name.clone()/>
|
||||
|
||||
@@ -14,10 +14,10 @@ This allows you to write components like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn Home(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn Home() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<main class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
@@ -48,7 +48,7 @@ This allows you to write components like this:
|
||||
use stylers::style;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
let styler_class = style! { "App",
|
||||
#two{
|
||||
color: blue;
|
||||
@@ -74,7 +74,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx, class = styler_class,
|
||||
view! { class = styler_class,
|
||||
<div class="one">
|
||||
<h1 id="two">"Hello"</h1>
|
||||
<h2>"World"</h2>
|
||||
@@ -93,7 +93,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
use styled::style;
|
||||
|
||||
#[component]
|
||||
pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
pub fn MyComponent() -> impl IntoView {
|
||||
let styles = style!(
|
||||
div {
|
||||
background-color: red;
|
||||
@@ -101,7 +101,7 @@ pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
);
|
||||
|
||||
styled::view! { cx, styles,
|
||||
styled::view! { styles,
|
||||
<div>"This text should be red with white text."</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ There’s a very simple way to determine whether you should use a capital-S `<Sc
|
||||
|
||||
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.
|
||||
|
||||
`<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"
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
[`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) is a specialized `<Form/>` that takes a server action, and automatically dispatches it on form submission. This allows you to call a server function directly from a `<form>`, even without JS/WASM.
|
||||
|
||||
The process is simple:
|
||||
|
||||
1. Define a server function using the [`#[server]` macro](https://docs.rs/leptos/latest/leptos/attr.server.html) (see [Server Functions](../server/25_server_functions.md).)
|
||||
2. Create an action using [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), specifying the type of the server function you’ve defined.
|
||||
3. Create an `<ActionForm/>`, providing the server action in the `action` prop.
|
||||
4. Pass the named arguments to the server function as form fields with the same names.
|
||||
|
||||
> **Note:** `<ActionForm/>` only works with the default URL-encoded `POST` encoding for server functions, to ensure graceful degradation/correct behavior as an HTML form.
|
||||
> **Note:** `<ActionForm/>` only works with the default URL-encoded `POST` encoding for server functions, to ensure graceful degradation/correct behavior as an HTML form.
|
||||
|
||||
```rust
|
||||
#[server(AddTodo, "/api")]
|
||||
@@ -17,14 +18,14 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AddTodo(cx: Scope) -> impl IntoView {
|
||||
let add_todo = create_server_action::<AddTodo>(cx);
|
||||
fn AddTodo() -> impl IntoView {
|
||||
let add_todo = create_server_action::<AddTodo>();
|
||||
// holds the latest *returned* value from the server
|
||||
let value = add_todo.value();
|
||||
// check if the server has returned an error
|
||||
let has_error = move || value.with(|val| matches!(val, Some(Err(_))));
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<ActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
@@ -36,6 +37,7 @@ fn AddTodo(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It’s really that easy. With JS/WASM, your form will submit without a page reload, storing its most recent submission in the `.input()` signal of the action, its pending status in `.pending()`, and so on. (See the [`Action`](https://docs.rs/leptos/latest/leptos/struct.Action.html) docs for a refresher, if you need.) Without JS/WASM, your form will submit with a page reload. If you call a `redirect` function (from `leptos_axum` or `leptos_actix`) it will redirect to the correct page. By default, it will redirect back to the page you’re currently on. The power of HTML, HTTP, and isomorphic rendering mean that your `<ActionForm/>` simply works, even with no JS/WASM.
|
||||
|
||||
## Client-Side Validation
|
||||
@@ -53,4 +55,4 @@ let on_submit = move |ev| {
|
||||
ev.prevent_default();
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -9,10 +9,10 @@ Hidden behind the whole reactive DOM renderer that we’ve seen so far is a func
|
||||
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
let (a, set_a) = create_signal(0);
|
||||
let (b, set_b) = create_signal(0);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
// immediately prints "Value: 0" and subscribes to `a`
|
||||
log::debug!("Value: {}", a());
|
||||
});
|
||||
@@ -42,15 +42,14 @@ While they’re not a “zero-cost abstraction” in the most technical sense—
|
||||
Imagine that I’m creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
|
||||
|
||||
```rust
|
||||
let (first, set_first) = create_signal(cx, String::new());
|
||||
let (last, set_last) = create_signal(cx, String::new());
|
||||
let (use_last, set_use_last) = create_signal(cx, true);
|
||||
let (first, set_first) = create_signal(String::new());
|
||||
let (last, set_last) = create_signal(String::new());
|
||||
let (use_last, set_use_last) = create_signal(true);
|
||||
|
||||
// this will add the name to the log
|
||||
// any time one of the source signals changes
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
log(
|
||||
cx,
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
@@ -77,9 +76,9 @@ If you need to synchronize some reactive value with the non-reactive world outsi
|
||||
We’ve managed to get this far without mentioning effects because they’re built into the Leptos DOM renderer. We’ve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{count}</p>
|
||||
}
|
||||
```
|
||||
@@ -87,13 +86,13 @@ view! { cx,
|
||||
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// create a DOM element
|
||||
let p = create_element("p");
|
||||
|
||||
// create an effect to reactively update the text
|
||||
create_effect(cx, move |prev_value| {
|
||||
create_effect(move |prev_value| {
|
||||
// first, access the signal’s value and convert it to a string
|
||||
let text = count().to_string();
|
||||
|
||||
@@ -119,10 +118,9 @@ In addition to `create_effect`, Leptos provides a [`watch`](https://docs.rs/lept
|
||||
Like `create_resource`, `watch` takes a first argument, which is reactively tracked, and a second, which is not. Whenever a reactive value in its `deps` argument is changed, the `callback` is run. `watch` returns a function that can be called to stop tracking the dependencies.
|
||||
|
||||
```rust
|
||||
let (num, set_num) = create_signal(cx, 0);
|
||||
let (num, set_num) = create_signal(0);
|
||||
|
||||
let stop = watch(
|
||||
cx,
|
||||
move || num.get(),
|
||||
move |num, prev_num, _| {
|
||||
log::debug!("Number: {}; Prev: {:?}", num, prev_num);
|
||||
@@ -149,30 +147,30 @@ use leptos::html::Input;
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
fn App() -> impl IntoView {
|
||||
// Just making a visible log here
|
||||
// You can ignore this...
|
||||
let log = create_rw_signal::<Vec<String>>(cx, vec![]);
|
||||
let log = create_rw_signal::<Vec<String>>(vec![]);
|
||||
let logged = move || log().join("\n");
|
||||
provide_context(cx, log);
|
||||
provide_context(log);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<CreateAnEffect/>
|
||||
<pre>{logged}</pre>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn CreateAnEffect(cx: Scope) -> impl IntoView {
|
||||
let (first, set_first) = create_signal(cx, String::new());
|
||||
let (last, set_last) = create_signal(cx, String::new());
|
||||
let (use_last, set_use_last) = create_signal(cx, true);
|
||||
fn CreateAnEffect() -> impl IntoView {
|
||||
let (first, set_first) = create_signal(String::new());
|
||||
let (last, set_last) = create_signal(String::new());
|
||||
let (use_last, set_use_last) = create_signal(true);
|
||||
|
||||
// this will add the name to the log
|
||||
// any time one of the source signals changes
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
log(
|
||||
cx,
|
||||
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
@@ -181,7 +179,7 @@ fn CreateAnEffect(cx: Scope) -> impl IntoView {
|
||||
)
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1><code>"create_effect"</code> " Version"</h1>
|
||||
<form>
|
||||
<label>
|
||||
@@ -207,14 +205,14 @@ fn CreateAnEffect(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ManualVersion(cx: Scope) -> impl IntoView {
|
||||
let first = create_node_ref::<Input>(cx);
|
||||
let last = create_node_ref::<Input>(cx);
|
||||
let use_last = create_node_ref::<Input>(cx);
|
||||
fn ManualVersion() -> impl IntoView {
|
||||
let first = create_node_ref::<Input>();
|
||||
let last = create_node_ref::<Input>();
|
||||
let use_last = create_node_ref::<Input>();
|
||||
|
||||
let mut prev_name = String::new();
|
||||
let on_change = move |_| {
|
||||
log(cx, " listener");
|
||||
log(" listener");
|
||||
let first = first.get().unwrap();
|
||||
let last = last.get().unwrap();
|
||||
let use_last = use_last.get().unwrap();
|
||||
@@ -225,12 +223,12 @@ fn ManualVersion(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
|
||||
if this_one != prev_name {
|
||||
log(cx, &this_one);
|
||||
log(&this_one);
|
||||
prev_name = this_one;
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"Manual Version"</h1>
|
||||
<form on:change=on_change>
|
||||
<label>
|
||||
@@ -257,12 +255,12 @@ fn ManualVersion(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
|
||||
let (my_value, set_my_value) = create_signal(cx, String::new());
|
||||
fn EffectVsDerivedSignal() -> impl IntoView {
|
||||
let (my_value, set_my_value) = create_signal(String::new());
|
||||
// Don't do this.
|
||||
/*let (my_optional_value, set_optional_my_value) = create_signal(cx, Option::<String>::None);
|
||||
/*let (my_optional_value, set_optional_my_value) = create_signal(Option::<String>::None);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
if !my_value.get().is_empty() {
|
||||
set_optional_my_value(Some(my_value.get()));
|
||||
} else {
|
||||
@@ -274,7 +272,7 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
|
||||
let my_optional_value =
|
||||
move || (!my_value.with(String::is_empty)).then(|| Some(my_value.get()));
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<input
|
||||
prop:value=my_value
|
||||
on:input= move |ev| set_my_value(event_target_value(&ev))
|
||||
@@ -286,7 +284,7 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
|
||||
<code>
|
||||
<Show
|
||||
when=move || my_optional_value().is_some()
|
||||
fallback=|cx| view! { cx, "None" }
|
||||
fallback=|| view! { "None" }
|
||||
>
|
||||
"Some(\"" {my_optional_value().unwrap()} "\")"
|
||||
</Show>
|
||||
@@ -298,9 +296,9 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
|
||||
/*#[component]
|
||||
pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
cx: Scope,
|
||||
|
||||
/// The components Show wraps
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
children: Box<dyn Fn() -> Fragment>,
|
||||
/// A closure that returns a bool that determines whether this thing runs
|
||||
when: W,
|
||||
/// A closure that returns what gets rendered if the when statement is false
|
||||
@@ -308,24 +306,24 @@ pub fn Show<F, W, IV>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
W: Fn() -> bool + 'static,
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
F: Fn() -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let memoized_when = create_memo(cx, move |_| when());
|
||||
let memoized_when = create_memo(move |_| when());
|
||||
|
||||
move || match memoized_when.get() {
|
||||
true => children(cx).into_view(cx),
|
||||
false => fallback(cx).into_view(cx),
|
||||
true => children().into_view(),
|
||||
false => fallback().into_view(),
|
||||
}
|
||||
}*/
|
||||
|
||||
fn log(cx: Scope, msg: impl std::fmt::Display) {
|
||||
let log = use_context::<RwSignal<Vec<String>>>(cx).unwrap();
|
||||
fn log(std::fmt::Display) {
|
||||
let log = use_context::<RwSignal<Vec<String>>>().unwrap();
|
||||
log.update(|log| log.push(msg.to_string()));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@ application. It sometimes looks a little silly:
|
||||
|
||||
```rust
|
||||
// a signal holds a value, and can be updated
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// a derived signal is a function that accesses other signals
|
||||
let double_count = move || count() * 2;
|
||||
@@ -19,11 +19,11 @@ let text = move || if count_is_odd() {
|
||||
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and reruns when they change
|
||||
create_effect(cx, move |_| {
|
||||
log!("text = {}", text());
|
||||
create_effect(move |_| {
|
||||
logging::log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{move || text().to_uppercase()}</p>
|
||||
}
|
||||
```
|
||||
@@ -53,12 +53,12 @@ Take our typical `<SimpleCounter/>` example in its simplest form:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
pub fn SimpleCounter() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button on:click=increment>
|
||||
{value}
|
||||
</button>
|
||||
|
||||
@@ -14,17 +14,17 @@ There are four basic signal operations:
|
||||
Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `WriteSignal` as a function is syntax sugar for `.set()`. So
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count(1);
|
||||
log!(count());
|
||||
logging::log!(count());
|
||||
```
|
||||
|
||||
is the same as
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
set_count.set(1);
|
||||
log!(count.get());
|
||||
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)`.
|
||||
@@ -36,7 +36,7 @@ However, there are some very good use cases for `.with()` and `.update()`.
|
||||
For example, consider a signal that holds a `Vec<String>`.
|
||||
|
||||
```rust
|
||||
let (names, set_names) = create_signal(cx, Vec::new());
|
||||
let (names, set_names) = create_signal(Vec::new());
|
||||
if names().is_empty() {
|
||||
set_names(vec!["Alice".to_string()]);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ In terms of logic, this is simple enough, but it’s hiding some significant ine
|
||||
Likewise, `set_names` replaces the value with a whole new `Vec<_>`. This is fine, but we might as well just mutate the original `Vec<_>` in place.
|
||||
|
||||
```rust
|
||||
let (names, set_names) = create_signal(cx, Vec::new());
|
||||
let (names, set_names) = create_signal(Vec::new());
|
||||
if names.with(|names| names.is_empty()) {
|
||||
set_names.update(|names| names.push("Alice".to_string()));
|
||||
}
|
||||
@@ -63,40 +63,74 @@ 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
|
||||
|
||||
Often people ask about situations in which some signal needs to change based on some other signal’s value. There are three good ways to do this, and one that’s less than ideal but okay under controlled circumstances.
|
||||
|
||||
### Good Options
|
||||
|
||||
**1) B is a function of A.** Create a signal for A and a derived signal or memo for B.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 1);
|
||||
let (count, set_count) = create_signal(1);
|
||||
let derived_signal_double_count = move || count() * 2;
|
||||
let memoized_double_count = create_memo(cx, move |_| count() * 2);
|
||||
let memoized_double_count = create_memo(move |_| count() * 2);
|
||||
```
|
||||
|
||||
> For guidance on whether to use a derived signal or a memo, see the docs for [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html)
|
||||
>
|
||||
**2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
|
||||
>
|
||||
> **2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
|
||||
|
||||
```rust
|
||||
let (first_name, set_first_name) = create_signal(cx, "Bridget".to_string());
|
||||
let (last_name, set_last_name) = create_signal(cx, "Jones".to_string());
|
||||
let (first_name, set_first_name) = create_signal("Bridget".to_string());
|
||||
let (last_name, set_last_name) = create_signal("Jones".to_string());
|
||||
let full_name = move || format!("{} {}", first_name(), last_name());
|
||||
```
|
||||
|
||||
**3) A and B are independent signals, but sometimes updated at the same time.** When you make the call to update A, make a separate call to update B.
|
||||
|
||||
```rust
|
||||
let (age, set_age) = create_signal(cx, 32);
|
||||
let (favorite_number, set_favorite_number) = create_signal(cx, 42);
|
||||
let (age, set_age) = create_signal(32);
|
||||
let (favorite_number, set_favorite_number) = create_signal(42);
|
||||
// use this to handle a click on a `Clear` button
|
||||
let clear_handler = move |_| {
|
||||
set_age(0);
|
||||
set_favorite_number(0);
|
||||
};
|
||||
```
|
||||
|
||||
### If you really must...
|
||||
|
||||
**4) Create an effect to write to B whenever A changes.** This is officially discouraged, for several reasons:
|
||||
a) It will always be less efficient, as it means every time A updates you do two full trips through the reactive process. (You set A, which causes the effect to run, as well as any other effects that depend on A. Then you set B, which causes any effects that depend on B to run.)
|
||||
b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals from effects.
|
||||
|
||||
@@ -33,8 +33,8 @@ use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
@@ -58,8 +58,8 @@ use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
@@ -83,21 +83,62 @@ The `path` can include
|
||||
- dynamic, named parameters beginning with a colon (`/:id`),
|
||||
- and/or a wildcard beginning with an asterisk (`/user/*any`)
|
||||
|
||||
The `view` is a function that takes a `Scope` and returns a view.
|
||||
The `view` is a function that returns a view. Any component with no props works here, as does a closure that returns some view.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=Home/>
|
||||
<Route path="/users" view=Users/>
|
||||
<Route path="/users/:id" view=UserProfile/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
<Route path="/*any" view=|| view! { <h1>"Not Found"</h1> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> `view` takes a `Fn(Scope) -> impl IntoView`. If a component has no props, it is a function that takes `Scope` and returns `impl IntoView`, so it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|cx| view! { cx, <Home/> }`.
|
||||
> `view` takes a `Fn() -> impl IntoView`. If a component has no props, it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|| view! { <Home/> }`.
|
||||
|
||||
Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` you’ll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
|
||||
|
||||
Note that you can define your routes in any order. The router scores each route to see how good a match it is, rather than simply trying to match them top to bottom.
|
||||
|
||||
Simple enough?
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -96,7 +96,7 @@ You can easily define this with nested routes
|
||||
<Routes>
|
||||
<Route path="/contacts" view=ContactList>
|
||||
<Route path=":id" view=ContactInfo/>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
@@ -113,7 +113,7 @@ You can go even deeper. Say you want to have tabs for each contact’s address,
|
||||
<Route path="address" view=Address/>
|
||||
<Route path="messages" view=Messages/>
|
||||
</Route>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
@@ -135,15 +135,15 @@ That’s all! But it’s important to know and to remember, because it’s a com
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
pub fn ContactList() -> impl IntoView {
|
||||
let contacts = todo!();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div style="display: flex">
|
||||
// the contact list
|
||||
<For each=contacts
|
||||
key=|contact| contact.id
|
||||
view=|cx, contact| todo!()
|
||||
view=|contact| todo!()
|
||||
>
|
||||
// the nested child, if any
|
||||
// don’t forget this!
|
||||
@@ -153,6 +153,43 @@ pub fn ContactList(cx: Scope) -> 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?
|
||||
@@ -179,8 +216,8 @@ use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<h1>"Contact App"</h1>
|
||||
// this <nav> will show on every routes,
|
||||
@@ -195,28 +232,29 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<main>
|
||||
<Routes>
|
||||
// / just has an un-nested "Home"
|
||||
<Route path="/" view=|cx| view! { cx,
|
||||
<Route path="/" view=|| view! {
|
||||
<h3>"Home"</h3>
|
||||
}/>
|
||||
// /contacts has nested routes
|
||||
<Route
|
||||
path="/contacts"
|
||||
view=ContactList
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
</div>
|
||||
}/>
|
||||
<Route path="conversations" view=|cx| view! { cx,
|
||||
<Route path="conversations" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Conversations)"
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
// if no id specified, fall back
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<div class="select-user">
|
||||
"Select a user to view contact info."
|
||||
</div>
|
||||
@@ -229,8 +267,8 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactList(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn ContactList() -> impl IntoView {
|
||||
view! {
|
||||
<div class="contact-list">
|
||||
// here's our contact list component itself
|
||||
<div class="contact-list-contacts">
|
||||
@@ -249,9 +287,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
fn ContactInfo() -> impl IntoView {
|
||||
// we can access the :id param reactively with `use_params_map`
|
||||
let params = use_params_map(cx);
|
||||
let params = use_params_map();
|
||||
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
|
||||
|
||||
// imagine we're loading data from an API here
|
||||
@@ -262,7 +300,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
_ => "User not found.",
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="contact-info">
|
||||
<h4>{name}</h4>
|
||||
<div class="tabs">
|
||||
@@ -278,7 +316,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -50,8 +50,8 @@ Now we can use them in a component. Imagine a URL that has both params and a que
|
||||
The typed versions return `Memo<Result<T, _>>`. It’s a Memo so it reacts to changes in the URL. It’s a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
|
||||
|
||||
```rust
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let query = use_query::<ContactSearch>(cx);
|
||||
let params = use_params::<ContactParams>();
|
||||
let query = use_query::<ContactSearch>();
|
||||
|
||||
// id: || -> usize
|
||||
let id = move || {
|
||||
@@ -66,8 +66,8 @@ let id = move || {
|
||||
The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
|
||||
|
||||
```rust
|
||||
let params = use_params_map(cx);
|
||||
let query = use_query_map(cx);
|
||||
let params = use_params_map();
|
||||
let query = use_query_map();
|
||||
|
||||
// id: || -> Option<String>
|
||||
let id = move || {
|
||||
@@ -94,8 +94,8 @@ use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<h1>"Contact App"</h1>
|
||||
// this <nav> will show on every routes,
|
||||
@@ -110,7 +110,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<main>
|
||||
<Routes>
|
||||
// / just has an un-nested "Home"
|
||||
<Route path="/" view=|cx| view! { cx,
|
||||
<Route path="/" view=|| view! {
|
||||
<h3>"Home"</h3>
|
||||
}/>
|
||||
// /contacts has nested routes
|
||||
@@ -120,19 +120,19 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
</div>
|
||||
}/>
|
||||
<Route path="conversations" view=|cx| view! { cx,
|
||||
<Route path="conversations" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Conversations)"
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
// if no id specified, fall back
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<div class="select-user">
|
||||
"Select a user to view contact info."
|
||||
</div>
|
||||
@@ -145,8 +145,8 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactList(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn ContactList() -> impl IntoView {
|
||||
view! {
|
||||
<div class="contact-list">
|
||||
// here's our contact list component itself
|
||||
<div class="contact-list-contacts">
|
||||
@@ -165,9 +165,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
fn ContactInfo() -> impl IntoView {
|
||||
// we can access the :id param reactively with `use_params_map`
|
||||
let params = use_params_map(cx);
|
||||
let params = use_params_map();
|
||||
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
|
||||
|
||||
// imagine we're loading data from an API here
|
||||
@@ -178,7 +178,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
_ => "User not found.",
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="contact-info">
|
||||
<h4>{name}</h4>
|
||||
<div class="tabs">
|
||||
@@ -194,7 +194,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -23,8 +23,9 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_
|
||||
Your most-used methods of navigating between pages should be with `<a>` and `<form>` elements or with the enhanced `<A/>` and `<Form/>` components. Using links and forms to navigate is the best solution for accessibility and graceful degradation.
|
||||
|
||||
On occasion, though, you’ll want to navigate programmatically, i.e., call a function that can navigate to a new page. In that case, you should use the [`use_navigate`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_navigate.html) function.
|
||||
|
||||
```rust
|
||||
let navigate = leptos_router::use_navigate(cx);
|
||||
let navigate = leptos_router::use_navigate();
|
||||
navigate("/somewhere", Default::default());
|
||||
```
|
||||
|
||||
@@ -46,8 +47,8 @@ use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<h1>"Contact App"</h1>
|
||||
// this <nav> will show on every routes,
|
||||
@@ -62,7 +63,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<main>
|
||||
<Routes>
|
||||
// / just has an un-nested "Home"
|
||||
<Route path="/" view=|cx| view! { cx,
|
||||
<Route path="/" view=|| view! {
|
||||
<h3>"Home"</h3>
|
||||
}/>
|
||||
// /contacts has nested routes
|
||||
@@ -72,19 +73,19 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
>
|
||||
// if no id specified, fall back
|
||||
<Route path=":id" view=ContactInfo>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Contact Info)"
|
||||
</div>
|
||||
}/>
|
||||
<Route path="conversations" view=|cx| view! { cx,
|
||||
<Route path="conversations" view=|| view! {
|
||||
<div class="tab">
|
||||
"(Conversations)"
|
||||
</div>
|
||||
}/>
|
||||
</Route>
|
||||
// if no id specified, fall back
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Route path="" view=|| view! {
|
||||
<div class="select-user">
|
||||
"Select a user to view contact info."
|
||||
</div>
|
||||
@@ -97,8 +98,8 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactList(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn ContactList() -> impl IntoView {
|
||||
view! {
|
||||
<div class="contact-list">
|
||||
// here's our contact list component itself
|
||||
<div class="contact-list-contacts">
|
||||
@@ -117,9 +118,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
fn ContactInfo() -> impl IntoView {
|
||||
// we can access the :id param reactively with `use_params_map`
|
||||
let params = use_params_map(cx);
|
||||
let params = use_params_map();
|
||||
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
|
||||
|
||||
// imagine we're loading data from an API here
|
||||
@@ -130,7 +131,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
_ => "User not found.",
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div class="contact-info">
|
||||
<h4>{name}</h4>
|
||||
<div class="tabs">
|
||||
@@ -146,7 +147,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -24,15 +24,15 @@ async fn fetch_results() {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FormExample(cx: Scope) -> impl IntoView {
|
||||
pub fn FormExample() -> impl IntoView {
|
||||
// reactive access to URL query strings
|
||||
let query = use_query_map(cx);
|
||||
let query = use_query_map();
|
||||
// search stored as ?q=
|
||||
let search = move || query().get("q").cloned().unwrap_or_default();
|
||||
// a resource driven by the search string
|
||||
let search_results = create_resource(cx, search, fetch_results);
|
||||
let search_results = create_resource(search, fetch_results);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search/>
|
||||
<input type="submit"/>
|
||||
@@ -51,7 +51,7 @@ This is a great pattern. The data flow is extremely clear: all data flows from t
|
||||
We can actually take it a step further and do something kind of clever:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search
|
||||
oninput="this.form.requestSubmit()"
|
||||
@@ -74,8 +74,8 @@ use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<h1><code>"<Form/>"</code></h1>
|
||||
<main>
|
||||
@@ -88,14 +88,14 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn FormExample(cx: Scope) -> impl IntoView {
|
||||
pub fn FormExample() -> impl IntoView {
|
||||
// reactive access to URL query
|
||||
let query = use_query_map(cx);
|
||||
let query = use_query_map();
|
||||
let name = move || query().get("name").cloned().unwrap_or_default();
|
||||
let number = move || query().get("number").cloned().unwrap_or_default();
|
||||
let select = move || query().get("select").cloned().unwrap_or_default();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
// read out the URL query strings
|
||||
<table>
|
||||
<tr>
|
||||
@@ -172,7 +172,7 @@ pub fn FormExample(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -31,9 +31,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BusyButton(cx: Scope) -> impl IntoView {
|
||||
pub fn BusyButton() -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<button on:click=move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
@@ -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:
|
||||
@@ -100,11 +111,26 @@ In other words, you have two choices:
|
||||
> **Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?**
|
||||
>
|
||||
> These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the JSON format.
|
||||
>
|
||||
>
|
||||
> The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. As we’ll see [in a later chapter](../progressive_enhancement), this isn’t always a great idea.
|
||||
>
|
||||
>
|
||||
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -23,12 +23,12 @@ The [`extract` function in `leptos_actix`](https://docs.rs/leptos_actix/latest/l
|
||||
```rust
|
||||
|
||||
#[server(ActixExtract, "/api")]
|
||||
pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
pub async fn actix_extract() -> Result<String, ServerFnError> {
|
||||
use leptos_actix::extract;
|
||||
use actix_web::dev::ConnectionInfo;
|
||||
use actix_web::web::{Data, Query};
|
||||
|
||||
extract(cx,
|
||||
extract(
|
||||
|search: Query<Search>, connection: ConnectionInfo| async move {
|
||||
format!(
|
||||
"search = {}\nconnection = {:?}",
|
||||
@@ -47,11 +47,11 @@ The syntax for the [`leptos_axum::extract`](https://docs.rs/leptos_axum/latest/l
|
||||
|
||||
```rust
|
||||
#[server(AxumExtract, "/api")]
|
||||
pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
|
||||
pub async fn axum_extract() -> Result<String, ServerFnError> {
|
||||
use axum::{extract::Query, http::Method};
|
||||
use leptos_axum::extract;
|
||||
|
||||
extract(cx, |method: Method, res: Query<MyQuery>| async move {
|
||||
extract(|method: Method, res: Query<MyQuery>| async move {
|
||||
format!("{method:?} and {}", res.q)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -8,12 +8,12 @@ Extractors provide an easy way to access request data inside server functions. L
|
||||
|
||||
```rust
|
||||
#[server(TeaAndCookies)]
|
||||
pub async fn tea_and_cookies(cx: Scope) -> Result<(), ServerFnError> {
|
||||
pub async fn tea_and_cookies() -> Result<(), ServerFnError> {
|
||||
use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue};
|
||||
use leptos_actix::ResponseOptions;
|
||||
|
||||
// pull ResponseOptions from context
|
||||
let response = expect_context::<ResponseOptions>(cx);
|
||||
let response = expect_context::<ResponseOptions>();
|
||||
|
||||
// set the HTTP status code
|
||||
response.set_status(StatusCode::IM_A_TEAPOT);
|
||||
@@ -35,14 +35,13 @@ Here’s a simplified example from our [`session_auth_axum` example](https://git
|
||||
```rust
|
||||
#[server(Login, "/api")]
|
||||
pub async fn login(
|
||||
cx: Scope,
|
||||
username: String,
|
||||
password: String,
|
||||
remember: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
// pull the DB pool and auth provider from context
|
||||
let pool = pool(cx)?;
|
||||
let auth = auth(cx)?;
|
||||
let pool = pool()?;
|
||||
let auth = auth()?;
|
||||
|
||||
// check whether the user exists
|
||||
let user: User = User::get_from_username(username, &pool)
|
||||
@@ -60,7 +59,7 @@ pub async fn login(
|
||||
auth.remember_user(remember.is_some());
|
||||
|
||||
// and redirect to the home page
|
||||
leptos_axum::redirect(cx, "/");
|
||||
leptos_axum::redirect("/");
|
||||
Ok(())
|
||||
}
|
||||
// if not, return an error
|
||||
|
||||
@@ -110,14 +110,14 @@ With blocking resources, I can do something like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn BlogPost(cx: Scope) -> impl IntoView {
|
||||
let post_data = create_blocking_resource(cx, /* load blog post */);
|
||||
let comment_data = create_resource(cx, /* load blog post */);
|
||||
view! { cx,
|
||||
pub fn BlogPost() -> impl IntoView {
|
||||
let post_data = create_blocking_resource(/* load blog post */);
|
||||
let comment_data = create_resource(/* load blog post */);
|
||||
view! {
|
||||
<Suspense fallback=|| ()>
|
||||
{move || {
|
||||
post_data.with(cx, |data| {
|
||||
view! { cx,
|
||||
post_data.with(|data| {
|
||||
view! {
|
||||
<Title text=data.title/>
|
||||
<Meta name="description" content=data.excerpt/>
|
||||
<article>
|
||||
|
||||
@@ -8,8 +8,8 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
leptos::log!("where do I run?");
|
||||
pub fn App() -> impl IntoView {
|
||||
logging::log!("where do I run?");
|
||||
// ... whatever
|
||||
}
|
||||
```
|
||||
@@ -57,15 +57,15 @@ One way to create a bug is by creating a mismatch between the HTML that’s sent
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
let data = if cfg!(target_arch = "wasm32") {
|
||||
vec![0, 1, 2]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
data.into_iter()
|
||||
.map(|value| view! { cx, <span>{value}</span> })
|
||||
.collect_view(cx)
|
||||
.map(|value| view! { <span>{value}</span> })
|
||||
.collect_view()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -87,49 +87,12 @@ 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(cx: Scope) -> impl IntoView {
|
||||
let (loaded, set_loaded) = create_signal(cx, false);
|
||||
|
||||
// create_effect only runs on the client
|
||||
create_effect(cx, move |_| {
|
||||
// do something like reading from localStorage
|
||||
set_loaded(true);
|
||||
});
|
||||
|
||||
move || {
|
||||
if loaded() {
|
||||
view! { cx, <p>"Hello, world!"</p> }.into_any()
|
||||
} else {
|
||||
view! { cx, <div class="loading">"Loading..."</div> }.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This one gives us the scary panic
|
||||
|
||||
```
|
||||
panicked at 'assertion failed: `(left == right)`
|
||||
left: `"DIV"`,
|
||||
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
|
||||
```
|
||||
|
||||
And a handy link to this page!
|
||||
|
||||
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
|
||||
|
||||
#### Solution
|
||||
|
||||
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
|
||||
|
||||
```rust
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
// do something like reading from localStorage
|
||||
request_animation_frame(move || set_loaded(true));
|
||||
});
|
||||
@@ -163,10 +126,10 @@ For example, say that I want to store something in the browser’s `localStorage
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
logging::log!("{storage:?}");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -176,11 +139,11 @@ But if I wrap it in an effect...
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
logging::log!("{storage:?}");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -14,8 +14,8 @@ For example, instead of embedding logic in a component directly like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(vec![Todo { /* ... */ }]);
|
||||
// ⚠️ this is hard to test because it's embedded in the component
|
||||
let num_remaining = move || todos.with(|todos| {
|
||||
todos.iter().filter(|todo| !todo.completed).sum()
|
||||
@@ -37,14 +37,14 @@ impl Todos {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
fn test_remaining() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
|
||||
pub fn TodoApp() -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(Todos(vec![Todo { /* ... */ }]));
|
||||
// ✅ this has a test associated with it
|
||||
let num_remaining = move || todos.with(Todos::num_remaining);
|
||||
}
|
||||
@@ -65,7 +65,7 @@ This is a fairly simple manual testing setup that uses the [`wasm-pack test`](ht
|
||||
|
||||
#### Sample Test
|
||||
|
||||
```rust
|
||||
````rust
|
||||
#[wasm_bindgen_test]
|
||||
fn clear() {
|
||||
let document = leptos::document();
|
||||
@@ -74,7 +74,7 @@ fn clear() {
|
||||
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
|| view! { <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
@@ -86,11 +86,29 @@ fn clear() {
|
||||
|
||||
clear.click();
|
||||
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
/* HTML expected */
|
||||
);
|
||||
```
|
||||
```rust
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), || {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
````
|
||||
|
||||
### [`wasm-bindgen-test` with `counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable/tests/web)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
@@ -22,10 +22,10 @@ I’ll give you the whole thing up front, then walk through it line by line.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count(3);
|
||||
@@ -49,18 +49,17 @@ used as a component in your Leptos application. We’ll see some of the other fe
|
||||
this macro in a couple chapters.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope) -> impl IntoView
|
||||
fn App() -> impl IntoView
|
||||
```
|
||||
|
||||
Every component is a function with the following characteristics
|
||||
|
||||
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
as its first argument. This `Scope` is our entrypoint into the reactive system.
|
||||
By convention, it’s usually named `cx`.
|
||||
2. You can include other arguments, which will be available as component “props.”
|
||||
3. Component functions return `impl IntoView`, which is an opaque type that includes
|
||||
1. It takes zero or more arguments of any type.
|
||||
2. It returns `impl IntoView`, which is an opaque type that includes
|
||||
anything you could return from a Leptos `view`.
|
||||
|
||||
> Component function arguments are gathered together into a single props struct which is built by the `view` macro as needed.
|
||||
|
||||
## The Component Body
|
||||
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
@@ -69,7 +68,7 @@ few reactive variables, define any side effects that run in response to those va
|
||||
changing, and describe the user interface.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
```
|
||||
|
||||
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
|
||||
@@ -85,7 +84,7 @@ current value, you’ll call `set_count.set(...)` (or `set_count(...)`).
|
||||
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
// define an event listener with on:
|
||||
on:click=move |_| {
|
||||
@@ -127,7 +126,7 @@ Leptos with `nightly` Rust, signals are already functions, so the closure is unn
|
||||
As a result, you can write a simpler view:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<button /* ... */>
|
||||
"Click me: "
|
||||
// identical to {move || count.get()}
|
||||
@@ -171,16 +170,16 @@ use leptos::*;
|
||||
// Components are the building blocks of your user interface
|
||||
// They define a reusable unit of behavior
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
fn App() -> impl IntoView {
|
||||
// here we create a reactive signal
|
||||
// and get a (getter, setter) pair
|
||||
// signals are the basic unit of change in the framework
|
||||
// we'll talk more about them later
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// the `view` macro is how we define the user interface
|
||||
// it uses an HTML-like format that can accept certain Rust values
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
// on:click will run whenever the `click` event fires
|
||||
// every event handler is defined as `on:{eventname}`
|
||||
@@ -221,6 +220,6 @@ fn App(cx: Scope) -> 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(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
@@ -12,10 +12,10 @@ increment a counter.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
@@ -73,9 +73,9 @@ class=("button-20", move || count() % 2 == 1)
|
||||
Individual CSS properties can be directly updated with a similar `style:` syntax.
|
||||
|
||||
```rust
|
||||
let (x, set_x) = create_signal(cx, 0);
|
||||
let (y, set_y) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
let (x, set_x) = create_signal(0);
|
||||
let (y, set_y) = create_signal(0);
|
||||
view! {
|
||||
<div
|
||||
style="position: absolute"
|
||||
style:left=move || format!("{}px", x() + 100)
|
||||
@@ -145,7 +145,7 @@ 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
|
||||
signal change 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
|
||||
are designed to solve this problem for expensive calculations.
|
||||
|
||||
@@ -159,7 +159,7 @@ are designed to solve this problem for expensive calculations.
|
||||
>
|
||||
> ```rust
|
||||
> let html = "<p>This HTML will be injected.</p>";
|
||||
> view! { cx,
|
||||
> view! {
|
||||
> <div inner_html=html/>
|
||||
> }
|
||||
> ```
|
||||
@@ -177,15 +177,15 @@ are designed to solve this problem for expensive calculations.
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
// a "derived signal" is a function that accesses other signals
|
||||
// we can use this to create reactive values that depend on the
|
||||
// values of one or more other signals
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
@@ -227,7 +227,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ per click.
|
||||
You _could_ do this by just creating two `<progress>` elements:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count, set_count) = create_signal(0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
value=count
|
||||
@@ -35,10 +35,8 @@ Instead, let’s create a `<ProgressBar/>` component.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn ProgressBar() -> impl IntoView {
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
// hmm... where will we get this from?
|
||||
@@ -64,10 +62,9 @@ In Leptos, you define props by giving additional arguments to the component func
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
// now this works
|
||||
@@ -81,9 +78,9 @@ Now we can use our component in the main `<App/>` component’s view.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
view! {
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
@@ -118,14 +115,13 @@ argument to the component function with `#[prop(optional)]`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -149,12 +145,11 @@ with `#[prop(default = ...)`.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -171,11 +166,11 @@ as the `progress` prop on another `<ProgressBar/>`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
@@ -199,7 +194,6 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F>(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
@@ -207,7 +201,7 @@ fn ProgressBar<F>(
|
||||
where
|
||||
F: Fn() -> i32 + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -224,11 +218,10 @@ This generic can also be specified inline:
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F: Fn() -> i32 + 'static>(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)] max: u16,
|
||||
progress: F,
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -255,14 +248,13 @@ reactive value.
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
progress: Signal<i32>
|
||||
) -> impl IntoView
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
@@ -271,18 +263,18 @@ fn ProgressBar(
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
// .into() converts `ReadSignal` to `Signal`
|
||||
<ProgressBar progress=count/>
|
||||
// use `Signal::derive()` to wrap a derived signal
|
||||
<ProgressBar progress=Signal::derive(cx, double_count)/>
|
||||
<ProgressBar progress=Signal::derive(double_count)/>
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -294,11 +286,10 @@ Note that you can’t specify optional generic props for a component. Let’s se
|
||||
```rust,compile_fail
|
||||
#[component]
|
||||
fn ProgressBar<F: Fn() -> i32 + 'static>(
|
||||
cx: Scope,
|
||||
#[prop(optional)] progress: Option<F>,
|
||||
) -> impl IntoView {
|
||||
progress.map(|progress| {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=100
|
||||
value=progress
|
||||
@@ -308,8 +299,8 @@ fn ProgressBar<F: Fn() -> i32 + 'static>(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<ProgressBar/>
|
||||
}
|
||||
}
|
||||
@@ -337,11 +328,10 @@ However, you can get around this by providing a concrete type using `Box<dyn _>`
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
#[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>,
|
||||
) -> impl IntoView {
|
||||
progress.map(|progress| {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max=100
|
||||
value=progress
|
||||
@@ -351,8 +341,8 @@ fn ProgressBar(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<ProgressBar/>
|
||||
}
|
||||
}
|
||||
@@ -376,7 +366,6 @@ component function, and each one of the props:
|
||||
/// Shows progress toward a goal.
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
/// The maximum value of the progress bar.
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
@@ -433,8 +422,6 @@ use leptos::*;
|
||||
/// Shows progress toward a goal.
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
// All components take a reactive `Scope` as the first argument
|
||||
cx: Scope,
|
||||
// Marks this as an optional prop. It will default to the default
|
||||
// value of its type, i.e., 0.
|
||||
#[prop(default = 100)]
|
||||
@@ -448,7 +435,7 @@ fn ProgressBar(
|
||||
/// How much progress should be displayed.
|
||||
progress: Signal<i32>,
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
view! {
|
||||
<progress
|
||||
max={max}
|
||||
value=progress
|
||||
@@ -458,12 +445,12 @@ fn ProgressBar(
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
@@ -481,12 +468,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<ProgressBar progress=count/>
|
||||
// Signal::derive creates a Signal wrapper from our derived signal
|
||||
// using double_count means it should move twice as fast
|
||||
<ProgressBar max=50 progress=Signal::derive(cx, double_count)/>
|
||||
<ProgressBar max=50 progress=Signal::derive(double_count)/>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -19,30 +19,30 @@ any `Vec<IV> where IV: IntoView` into your view. In other words, if you can rend
|
||||
|
||||
```rust
|
||||
let values = vec![0, 1, 2];
|
||||
view! { cx,
|
||||
view! {
|
||||
// this will just render "012"
|
||||
<p>{values.clone()}</p>
|
||||
// or we can wrap them in <li>
|
||||
<ul>
|
||||
{values.into_iter()
|
||||
.map(|n| view! { cx, <li>{n}</li>})
|
||||
.map(|n| view! { <li>{n}</li>})
|
||||
.collect::<Vec<_>>()}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
|
||||
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
|
||||
Leptos also provides a `.collect_view()` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
|
||||
|
||||
```rust
|
||||
let values = vec![0, 1, 2];
|
||||
view! { cx,
|
||||
view! {
|
||||
// this will just render "012"
|
||||
<p>{values.clone()}</p>
|
||||
// or we can wrap them in <li>
|
||||
<ul>
|
||||
{values.into_iter()
|
||||
.map(|n| view! { cx, <li>{n}</li>})
|
||||
.collect_view(cx)}
|
||||
.map(|n| view! { <li>{n}</li>})
|
||||
.collect_view()}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
@@ -52,13 +52,13 @@ You can render dynamic items as part of a static list.
|
||||
|
||||
```rust
|
||||
// create a list of N signals
|
||||
let counters = (1..=length).map(|idx| create_signal(cx, idx));
|
||||
let counters = (1..=length).map(|idx| create_signal(idx));
|
||||
|
||||
// each item manages a reactive view
|
||||
// but the list itself will never change
|
||||
let counter_buttons = counters
|
||||
.map(|(count, set_count)| {
|
||||
view! { cx,
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
on:click=move |_| set_count.update(|n| *n += 1)
|
||||
@@ -68,9 +68,9 @@ let counter_buttons = counters
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx);
|
||||
.collect_view();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<ul>{counter_buttons}</ul>
|
||||
}
|
||||
```
|
||||
@@ -120,8 +120,8 @@ use leptos::*;
|
||||
// 2) for lists that grow, shrink, or move items, using <For/>
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Iteration"</h1>
|
||||
<h2>"Static List"</h2>
|
||||
<p>"Use this pattern if the list itself is static."</p>
|
||||
@@ -136,19 +136,19 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
/// to add or remove any.
|
||||
#[component]
|
||||
fn StaticList(
|
||||
cx: Scope,
|
||||
|
||||
/// How many counters to include in this list.
|
||||
length: usize,
|
||||
) -> impl IntoView {
|
||||
// create counter signals that start at incrementing numbers
|
||||
let counters = (1..=length).map(|idx| create_signal(cx, idx));
|
||||
let counters = (1..=length).map(|idx| create_signal(idx));
|
||||
|
||||
// when you have a list that doesn't change, you can
|
||||
// manipulate it using ordinary Rust iterators
|
||||
// and collect it into a Vec<_> to insert it into the DOM
|
||||
let counter_buttons = counters
|
||||
.map(|(count, set_count)| {
|
||||
view! { cx,
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
on:click=move |_| set_count.update(|n| *n += 1)
|
||||
@@ -163,7 +163,7 @@ fn StaticList(
|
||||
// Note that if `counter_buttons` were a reactive list
|
||||
// and its value changed, this would be very inefficient:
|
||||
// it would rerender every row every time the list changed.
|
||||
view! { cx,
|
||||
view! {
|
||||
<ul>{counter_buttons}</ul>
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@ fn StaticList(
|
||||
/// remove counters.
|
||||
#[component]
|
||||
fn DynamicList(
|
||||
cx: Scope,
|
||||
|
||||
/// The number of counters to begin with.
|
||||
initial_length: usize,
|
||||
) -> impl IntoView {
|
||||
@@ -190,17 +190,17 @@ fn DynamicList(
|
||||
// we generate an initial list as in <StaticList/>
|
||||
// but this time we include the ID along with the signal
|
||||
let initial_counters = (0..initial_length)
|
||||
.map(|id| (id, create_signal(cx, id + 1)))
|
||||
.map(|id| (id, create_signal(id + 1)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// now we store that initial list in a signal
|
||||
// this way, we'll be able to modify the list over time,
|
||||
// adding and removing counters, and it will change reactively
|
||||
let (counters, set_counters) = create_signal(cx, initial_counters);
|
||||
let (counters, set_counters) = create_signal(initial_counters);
|
||||
|
||||
let add_counter = move |_| {
|
||||
// create a signal for the new counter
|
||||
let sig = create_signal(cx, next_counter_id + 1);
|
||||
let sig = create_signal(next_counter_id + 1);
|
||||
// add this counter to the list of counters
|
||||
set_counters.update(move |counters| {
|
||||
// since `.update()` gives us `&mut T`
|
||||
@@ -211,7 +211,7 @@ fn DynamicList(
|
||||
next_counter_id += 1;
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
@@ -231,8 +231,8 @@ fn DynamicList(
|
||||
key=|counter| counter.0
|
||||
// the view function receives each item from your `each` iterator
|
||||
// and returns a view
|
||||
view=move |cx, (id, (count, set_count))| {
|
||||
view! { cx,
|
||||
view=move |(id, (count, set_count))| {
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
on:click=move |_| set_count.update(|n| *n += 1)
|
||||
@@ -258,7 +258,7 @@ fn DynamicList(
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -23,9 +23,9 @@ There are two important things to remember:
|
||||
on an `<input type="checkbox">`.)
|
||||
|
||||
```rust
|
||||
let (name, set_name) = create_signal(cx, "Controlled".to_string());
|
||||
let (name, set_name) = create_signal("Controlled".to_string());
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<input type="text"
|
||||
on:input=move |ev| {
|
||||
// event_target_value is a Leptos helper function
|
||||
@@ -81,9 +81,9 @@ In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
event.
|
||||
|
||||
```rust
|
||||
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
let (name, set_name) = create_signal("Uncontrolled".to_string());
|
||||
|
||||
let input_element: NodeRef<Input> = create_node_ref(cx);
|
||||
let input_element: NodeRef<Input> = create_node_ref();
|
||||
```
|
||||
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
@@ -117,7 +117,7 @@ We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
gives us access to a correctly-typed HTML element.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<form on:submit=on_submit>
|
||||
<input type="text"
|
||||
value=name
|
||||
@@ -148,8 +148,8 @@ The view should be pretty self-explanatory by now. Note two things:
|
||||
use leptos::{ev::SubmitEvent, *};
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
fn App() -> impl IntoView {
|
||||
view! {
|
||||
<h2>"Controlled Component"</h2>
|
||||
<ControlledComponent/>
|
||||
<h2>"Uncontrolled Component"</h2>
|
||||
@@ -158,11 +158,11 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ControlledComponent(cx: Scope) -> impl IntoView {
|
||||
fn ControlledComponent() -> impl IntoView {
|
||||
// create a signal to hold the value
|
||||
let (name, set_name) = create_signal(cx, "Controlled".to_string());
|
||||
let (name, set_name) = create_signal("Controlled".to_string());
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<input type="text"
|
||||
// fire an event whenever the input changes
|
||||
on:input=move |ev| {
|
||||
@@ -192,15 +192,15 @@ fn ControlledComponent(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn UncontrolledComponent(cx: Scope) -> impl IntoView {
|
||||
fn UncontrolledComponent() -> impl IntoView {
|
||||
// import the type for <input>
|
||||
use leptos::html::Input;
|
||||
|
||||
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
let (name, set_name) = create_signal("Uncontrolled".to_string());
|
||||
|
||||
// we'll use a NodeRef to store a reference to the input element
|
||||
// this will be filled when the element is created
|
||||
let input_element: NodeRef<Input> = create_node_ref(cx);
|
||||
let input_element: NodeRef<Input> = create_node_ref();
|
||||
|
||||
// fires when the form `submit` event happens
|
||||
// this will store the value of the <input> in our signal
|
||||
@@ -220,7 +220,7 @@ fn UncontrolledComponent(cx: Scope) -> impl IntoView {
|
||||
set_name(value);
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<form on:submit=on_submit>
|
||||
<input type="text"
|
||||
// here, we use the `value` *attribute* to set only
|
||||
@@ -242,7 +242,7 @@ fn UncontrolledComponent(cx: Scope) -> 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(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -38,7 +38,7 @@ special knowledge.
|
||||
For example, let’s start with a simple signal and derived signal:
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, set_value) = create_signal(0);
|
||||
let is_odd = move || value() & 1 == 1;
|
||||
```
|
||||
|
||||
@@ -54,7 +54,7 @@ Let’s say I want to render some text if the number is odd, and some other text
|
||||
if it’s even. Well, how about this?
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>
|
||||
{move || if is_odd() {
|
||||
"Odd"
|
||||
@@ -81,7 +81,7 @@ let message = move || {
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
@@ -90,7 +90,7 @@ This works fine. We can make it a little shorter if we’d like, using `bool::th
|
||||
|
||||
```rust
|
||||
let message = move || is_odd().then(|| "Ding ding ding!");
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
@@ -112,7 +112,7 @@ let message = move || {
|
||||
_ => "Even"
|
||||
}
|
||||
};
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
@@ -131,7 +131,7 @@ above, where the value switches from even to odd on every change, this is fine.
|
||||
But consider the following example:
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
let message = move || if value() > 5 {
|
||||
"Big"
|
||||
@@ -139,7 +139,7 @@ let message = move || if value() > 5 {
|
||||
"Small"
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
};
|
||||
```
|
||||
@@ -194,12 +194,12 @@ the answer. You pass it a `when` condition function, a `fallback` to be shown if
|
||||
the `when` function returns `false`, and children to be rendered if `when` is `true`.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<Show
|
||||
when=move || { value() > 5 }
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
fallback=|| view! { <Small/> }
|
||||
>
|
||||
<Big/>
|
||||
</Show>
|
||||
@@ -228,19 +228,19 @@ can be a little annoying if you’re returning different HTML elements from
|
||||
different branches of a conditional:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
view! {
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }
|
||||
view! { <pre>"One"</pre> }
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }
|
||||
view! { <p>"Two"</p> }
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }
|
||||
_ => view! { <textarea>{value()}</textarea> }
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
@@ -260,24 +260,24 @@ to get yourself out of this situation:
|
||||
1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>`
|
||||
with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any)
|
||||
2. If you have a variety of view types that are not all `HtmlElement`, convert them to
|
||||
`View`s with [`.into_view(cx)`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
|
||||
`View`s with [`.into_view()`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
|
||||
|
||||
Here’s the same example, with the conversion added:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
view! {
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }.into_any()
|
||||
view! { <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }.into_any()
|
||||
view! { <p>"Two"</p> }.into_any()
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||
_ => view! { <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
@@ -294,12 +294,12 @@ view! { cx,
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
fn App() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(0);
|
||||
let is_odd = move || value() & 1 == 1;
|
||||
let odd_text = move || if is_odd() { Some("How odd!") } else { None };
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"Control Flow"</h1>
|
||||
|
||||
// Simple UI to update and show a value
|
||||
@@ -346,37 +346,37 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
// needed. This makes it more efficient in many cases
|
||||
// than a {move || if ...} block
|
||||
<Show when=is_odd
|
||||
fallback=|cx| view! { cx, <p>"Even steven"</p> }
|
||||
fallback=|| view! { <p>"Even steven"</p> }
|
||||
>
|
||||
<p>"Oddment"</p>
|
||||
</Show>
|
||||
|
||||
// d. Because `bool::then()` converts a `bool` to
|
||||
// `Option`, you can use it to create a show/hide toggled
|
||||
{move || is_odd().then(|| view! { cx, <p>"Oddity!"</p> })}
|
||||
{move || is_odd().then(|| view! { <p>"Oddity!"</p> })}
|
||||
|
||||
<h2>"Converting between Types"</h2>
|
||||
// e. Note: if branches return different types,
|
||||
// you can convert between them with
|
||||
// `.into_any()` (for different HTML element types)
|
||||
// or `.into_view(cx)` (for all view types)
|
||||
// or `.into_view()` (for all view types)
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// <pre> returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }.into_any()
|
||||
view! { <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// <p> returns HtmlElement<P>
|
||||
// so we convert into a more generic type
|
||||
view! { cx, <p>"Two"</p> }.into_any()
|
||||
view! { <p>"Two"</p> }.into_any()
|
||||
}
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||
_ => view! { <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -10,16 +10,16 @@ Let’s start with a simple component to capture a number input.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
fn NumericInput() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(Ok(0));
|
||||
|
||||
// when input changes, try to parse a number from the input
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<label>
|
||||
"Type a number (or not!)"
|
||||
<input on:input=on_input/>
|
||||
<input type="number" on:input=on_input/>
|
||||
<p>
|
||||
"You entered "
|
||||
<strong>{value}</strong>
|
||||
@@ -60,27 +60,27 @@ Let’s add an `<ErrorBoundary/>` to this example.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
fn NumericInput() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(Ok(0));
|
||||
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input on:input=on_input/>
|
||||
<input type="number" on:input=on_input/>
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|cx, errors| view! { cx,
|
||||
fallback=|errors| view! {
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect_view(cx)
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
|
||||
.collect_view()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -121,13 +121,13 @@ an `<ErrorBoundary/>` will appear again.
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
fn App() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(Ok(0));
|
||||
|
||||
// when input changes, try to parse a number from the input
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
@@ -137,7 +137,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
// <ErrorBoundary/> will be displayed.
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|cx, errors| view! { cx,
|
||||
fallback=|errors| view! {
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors
|
||||
@@ -145,7 +145,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
@@ -167,7 +167,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -29,17 +29,17 @@ it in the child. This lets you manipulate the state of the parent from the child
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonA setter=set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn ButtonA(setter: WriteSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
@@ -62,9 +62,9 @@ Another approach would be to pass a callback to the child: say, `on_click`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
@@ -72,14 +72,11 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
cx: Scope,
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
pub fn ButtonB<F>(on_click: F) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<button on:click=on_click>
|
||||
"Toggle"
|
||||
</button>
|
||||
@@ -105,9 +102,9 @@ in your `view` macro in `<App/>`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
// note the on:click instead of on_click
|
||||
// this is the same syntax as an HTML element event listener
|
||||
@@ -117,8 +114,8 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn ButtonC<F>() -> impl IntoView {
|
||||
view! {
|
||||
<button>"Toggle"</button>
|
||||
}
|
||||
}
|
||||
@@ -141,17 +138,17 @@ tree:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Layout() -> impl IntoView {
|
||||
view! {
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
</header>
|
||||
@@ -162,8 +159,8 @@ pub fn Layout(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Content() -> impl IntoView {
|
||||
view! {
|
||||
<div class="content">
|
||||
<ButtonD/>
|
||||
</div>
|
||||
@@ -171,7 +168,7 @@ pub fn Content(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD<F>(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonD<F>() -> impl IntoView {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
@@ -182,17 +179,17 @@ pass your `WriteSignal` to its props. You could do what’s sometimes called
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Layout(d: WriteSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
</header>
|
||||
@@ -203,8 +200,8 @@ pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
pub fn Content(d: WriteSignal<bool>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="content">
|
||||
<ButtonD set_toggled/>
|
||||
</div>
|
||||
@@ -212,7 +209,7 @@ pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD<F>(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
pub fn ButtonD<F>(d: WriteSignal<bool>) -> impl IntoView {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
@@ -237,13 +234,13 @@ unnecessary prop drilling.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
pub fn App() -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(false);
|
||||
|
||||
// share `set_toggled` with all children of this component
|
||||
provide_context(cx, set_toggled);
|
||||
provide_context(set_toggled);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
@@ -252,14 +249,14 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// <Layout/> and <Content/> omitted
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonD() -> impl IntoView {
|
||||
// use_context searches up the context tree, hoping to
|
||||
// find a `WriteSignal<bool>`
|
||||
// in this case, I .expect() because I know I provided it
|
||||
let setter = use_context::<WriteSignal<bool>>(cx)
|
||||
let setter = use_context::<WriteSignal<bool>>()
|
||||
.expect("to have found the setter provided");
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
@@ -308,20 +305,20 @@ use leptos::{ev::MouseEvent, *};
|
||||
struct SmallcapsContext(WriteSignal<bool>);
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn App() -> impl IntoView {
|
||||
// just some signals to toggle three classes on our <p>
|
||||
let (red, set_red) = create_signal(cx, false);
|
||||
let (right, set_right) = create_signal(cx, false);
|
||||
let (italics, set_italics) = create_signal(cx, false);
|
||||
let (smallcaps, set_smallcaps) = create_signal(cx, false);
|
||||
let (red, set_red) = create_signal(false);
|
||||
let (right, set_right) = create_signal(false);
|
||||
let (italics, set_italics) = create_signal(false);
|
||||
let (smallcaps, set_smallcaps) = create_signal(false);
|
||||
|
||||
// the newtype pattern isn't *necessary* here but is a good practice
|
||||
// it avoids confusion with other possible future `WriteSignal<bool>` contexts
|
||||
// and makes it easier to refer to it in ButtonC
|
||||
provide_context(cx, SmallcapsContext(set_smallcaps));
|
||||
provide_context(SmallcapsContext(set_smallcaps));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
<main>
|
||||
<p
|
||||
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
|
||||
@@ -353,12 +350,12 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// Button A receives a signal setter and updates the signal itself
|
||||
#[component]
|
||||
pub fn ButtonA(
|
||||
cx: Scope,
|
||||
|
||||
/// Signal that will be toggled when the button is clicked.
|
||||
setter: WriteSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
@@ -370,7 +367,7 @@ pub fn ButtonA(
|
||||
/// Button B receives a closure
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
cx: Scope,
|
||||
|
||||
/// Callback that will be invoked when the button is clicked.
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
@@ -378,7 +375,7 @@ where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
view! {
|
||||
cx,
|
||||
|
||||
<button
|
||||
on:click=on_click
|
||||
>
|
||||
@@ -402,9 +399,9 @@ where
|
||||
/// Button C is a dummy: it renders a button but doesn't handle
|
||||
/// its click. Instead, the parent component adds an event listener.
|
||||
#[component]
|
||||
pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonC() -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
|
||||
<button>
|
||||
"Toggle Italics"
|
||||
</button>
|
||||
@@ -414,11 +411,11 @@ pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
/// Button D is very similar to Button A, but instead of passing the setter as a prop
|
||||
/// we get it from the context
|
||||
#[component]
|
||||
pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||
let setter = use_context::<SmallcapsContext>(cx).unwrap().0;
|
||||
pub fn ButtonD() -> impl IntoView {
|
||||
let setter = use_context::<SmallcapsContext>().unwrap().0;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
@@ -428,7 +425,7 @@ pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ children into an HTML element. For example, imagine I have a `<FancyForm/>` comp
|
||||
that enhances an HTML `<form>`. I need some way to pass all its inputs.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<Form>
|
||||
<fieldset>
|
||||
<label>
|
||||
@@ -28,12 +28,12 @@ other components:
|
||||
In fact, you’ve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<Show
|
||||
// `when` is a normal prop
|
||||
when=move || value() > 5
|
||||
// `fallback` is a "render prop": a function that returns a view
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
fallback=|| view! { <Small/> }
|
||||
>
|
||||
// `<Big/>` (and anything else here)
|
||||
// will be given to the `children` prop
|
||||
@@ -47,7 +47,6 @@ Let’s define a component that takes some children and a render prop.
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
cx: Scope,
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
@@ -58,19 +57,19 @@ where
|
||||
F: Fn() -> IV,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<h2>"Render Prop"</h2>
|
||||
{render_prop()}
|
||||
|
||||
<h2>"Children"</h2>
|
||||
{children(cx)}
|
||||
{children()}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`render_prop` and `children` are both functions, so we can call them to generate
|
||||
the appropriate views. `children`, in particular, is an alias for
|
||||
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
|
||||
`Box<dyn FnOnce() -> Fragment>`. (Aren't you glad we named it `Children` instead?)
|
||||
|
||||
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
|
||||
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
|
||||
@@ -78,8 +77,8 @@ the appropriate views. `children`, in particular, is an alias for
|
||||
We can use the component like this:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
|
||||
view! {
|
||||
<TakesChildren render_prop=|| view! { <p>"Hi, there!"</p> }>
|
||||
// these get passed to `children`
|
||||
"Some text"
|
||||
<span>"A span"</span>
|
||||
@@ -97,15 +96,15 @@ a component that takes its children and turns them into an unordered list.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
pub fn WrapsChildren(Children) -> impl IntoView {
|
||||
// Fragment has `nodes` field that contains a Vec<View>
|
||||
let children = children(cx)
|
||||
let children = children()
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.collect_view(cx);
|
||||
.map(|child| view! { <li>{child}</li> })
|
||||
.collect_view();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<ul>{children}</ul>
|
||||
}
|
||||
}
|
||||
@@ -114,7 +113,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
Calling it like this will create a list:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
view! {
|
||||
<WrapsChildren>
|
||||
"A"
|
||||
"B"
|
||||
@@ -142,19 +141,19 @@ use leptos::*;
|
||||
// property
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (items, set_items) = create_signal(cx, vec![0, 1, 2]);
|
||||
pub fn App() -> impl IntoView {
|
||||
let (items, set_items) = create_signal(vec![0, 1, 2]);
|
||||
let render_prop = move || {
|
||||
// items.with(...) reacts to the value without cloning
|
||||
// by applying a function. Here, we pass the `len` method
|
||||
// on a `Vec<_>` directly
|
||||
let len = move || items.with(Vec::len);
|
||||
view! { cx,
|
||||
view! {
|
||||
<p>"Length: " {len}</p>
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
// This component just displays the two kinds of children,
|
||||
// embedding them in some other markup
|
||||
<TakesChildren
|
||||
@@ -179,12 +178,12 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// Displays a `render_prop` and some children within markup.
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
cx: Scope,
|
||||
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
/// `children` takes the `Children` type
|
||||
/// this is an alias for `Box<dyn FnOnce(Scope) -> Fragment>`
|
||||
/// this is an alias for `Box<dyn FnOnce() -> Fragment>`
|
||||
/// ... aren't you glad we named it `Children` instead?
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
@@ -192,30 +191,30 @@ where
|
||||
F: Fn() -> IV,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1><code>"<TakesChildren/>"</code></h1>
|
||||
<h2>"Render Prop"</h2>
|
||||
{render_prop()}
|
||||
<hr/>
|
||||
<h2>"Children"</h2>
|
||||
{children(cx)}
|
||||
{children()}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps each child in an `<li>` and embeds them in a `<ul>`.
|
||||
#[component]
|
||||
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
// children(cx) returns a `Fragment`, which has a
|
||||
pub fn WrapsChildren(Children) -> impl IntoView {
|
||||
// children() returns a `Fragment`, which has a
|
||||
// `nodes` field that contains a Vec<View>
|
||||
// this means we can iterate over the children
|
||||
// to create something new!
|
||||
let children = children(cx)
|
||||
let children = children()
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.map(|child| view! { <li>{child}</li> })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<h1><code>"<WrapsChildren/>"</code></h1>
|
||||
// wrap our wrapped children in a UL
|
||||
<ul>{children}</ul>
|
||||
@@ -223,7 +222,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
leptos::mount_to_body(|| view! { <App/> })
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -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,45 @@
|
||||
# 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.4.9` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.4.9/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_**)
|
||||
- [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,11 +2,11 @@ use core::time::Duration;
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let show = create_rw_signal(cx, false);
|
||||
pub fn App() -> impl IntoView {
|
||||
let show = create_rw_signal(false);
|
||||
|
||||
// the CSS classes in this example are just written directly inside the `index.html`
|
||||
view! { cx,
|
||||
view! {
|
||||
<div
|
||||
class="hover-me"
|
||||
on:mouseenter=move |_| show.set(true)
|
||||
|
||||
@@ -4,9 +4,5 @@ use leptos::*;
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
<App />
|
||||
}
|
||||
})
|
||||
mount_to_body(App);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5,15 +5,14 @@ use leptos::*;
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleCounter(
|
||||
cx: Scope,
|
||||
/// The starting value for the counter
|
||||
initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
step: i32,
|
||||
) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
|
||||
@@ -4,8 +4,8 @@ use leptos::*;
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
mount_to_body(|| {
|
||||
view! {
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
|
||||
@@ -15,7 +15,7 @@ fn clear() {
|
||||
// note that we start at the initial value of 10
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
|| view! { <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
@@ -32,16 +32,17 @@ fn clear() {
|
||||
|
||||
// now let's test the <div> against the expected value
|
||||
// we can do this by testing its `outerHTML`
|
||||
let runtime = create_runtime();
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system, just to render the
|
||||
// test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
{
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, _set_value) = create_signal(cx, 0);
|
||||
let (value, _set_value) = create_signal(0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
@@ -52,7 +53,7 @@ fn clear() {
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
// There's actually an easier way to do this...
|
||||
@@ -61,10 +62,12 @@ fn clear() {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
|| view! { <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
@@ -75,7 +78,7 @@ fn inc() {
|
||||
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> },
|
||||
|| view! { <SimpleCounter initial_value=0 step=1/> },
|
||||
);
|
||||
|
||||
// You can do testing with vanilla DOM operations
|
||||
@@ -118,12 +121,14 @@ fn inc() {
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
|
||||
let runtime = create_runtime();
|
||||
|
||||
// Or you can test against a sample view!
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
run_scope(create_runtime(), |cx| {
|
||||
let (value, _) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
{
|
||||
let (value, _) = create_signal(0);
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
@@ -132,17 +137,17 @@ fn inc() {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
.outer_html())
|
||||
.outer_html()
|
||||
);
|
||||
|
||||
inc.click();
|
||||
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
run_scope(create_runtime(), |cx| {
|
||||
{
|
||||
// because we've clicked, it's as if the signal is starting at 1
|
||||
let (value, _) = create_signal(cx, 1);
|
||||
view! { cx,
|
||||
let (value, _) = create_signal(1);
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
@@ -151,6 +156,8 @@ fn inc() {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
.outer_html())
|
||||
.outer_html()
|
||||
);
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
@@ -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.87"
|
||||
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,16 +32,16 @@ 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;
|
||||
Ok(0)
|
||||
}
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! { cx,
|
||||
pub fn Counters() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
view! {
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
@@ -67,10 +66,24 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=Counter/>
|
||||
<Route path="form" view=FormCounter/>
|
||||
<Route path="multi" view=MultiuserCounter/>
|
||||
<Route path="multi" view=NotFound/>
|
||||
<Route
|
||||
path=""
|
||||
view=|| {
|
||||
view! { <Counter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="form"
|
||||
view=|| {
|
||||
view! { <FormCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="multi"
|
||||
view=|| {
|
||||
view! { <MultiuserCounter/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -82,12 +95,11 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
// it's invalidated by one of the user's own actions
|
||||
// This is the typical pattern for a CRUD app
|
||||
#[component]
|
||||
pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let dec = create_action(|_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || {
|
||||
(
|
||||
dec.version().get(),
|
||||
@@ -98,20 +110,16 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
let value = move || {
|
||||
counter
|
||||
.read(cx)
|
||||
.map(|count| count.unwrap_or(0))
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let value =
|
||||
move || counter.get().map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let error_msg = move || {
|
||||
counter.read(cx).and_then(|res| match res {
|
||||
counter.get().and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<h2>"Simple Counter"</h2>
|
||||
<p>
|
||||
@@ -126,7 +134,7 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
{move || {
|
||||
error_msg()
|
||||
.map(|msg| {
|
||||
view! { cx, <p>"Error: " {msg.to_string()}</p> }
|
||||
view! { <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
@@ -137,12 +145,13 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
// It uses the same invalidation pattern as the plain counter,
|
||||
// but uses HTML forms to submit the actions
|
||||
#[component]
|
||||
pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
let adjust = create_server_action::<AdjustServerCount>(cx);
|
||||
let clear = create_server_action::<ClearServerCount>(cx);
|
||||
pub fn FormCounter() -> impl IntoView {
|
||||
// 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>();
|
||||
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || (adjust.version().get(), clear.version().get()),
|
||||
|_| {
|
||||
log::debug!("FormCounter running fetcher");
|
||||
@@ -151,19 +160,23 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let value = move || {
|
||||
log::debug!("FormCounter looking for value");
|
||||
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
|
||||
counter.get().and_then(|n| n.ok()).unwrap_or(0)
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<h2>"Form Counter"</h2>
|
||||
<p>
|
||||
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
|
||||
</p>
|
||||
<div>
|
||||
// calling a server function is the same as POSTing to its API URL
|
||||
// so we can just do that with a form and button
|
||||
<ActionForm action=clear>
|
||||
<input type="submit" value="Clear"/>
|
||||
</ActionForm>
|
||||
// We can submit named arguments to the server functions
|
||||
// by including them as input values with the same name
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="-1"/>
|
||||
<input type="hidden" name="msg" value="form value down"/>
|
||||
@@ -185,12 +198,11 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
// Whenever another user updates the value, it will update here
|
||||
// This is the primitive pattern for live chat, collaborative editing, etc.
|
||||
#[component]
|
||||
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
pub fn MultiuserCounter() -> impl IntoView {
|
||||
let dec =
|
||||
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
create_action(|_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
@@ -200,7 +212,6 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source
|
||||
.subscribe("message")
|
||||
.unwrap()
|
||||
@@ -214,14 +225,14 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
}),
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
on_cleanup(move || source.close());
|
||||
s
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
|
||||
let (multiplayer_value, _) = create_signal(None::<i32>);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<h2>"Multi-User Counter"</h2>
|
||||
<p>
|
||||
@@ -238,14 +249,3 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NotFound(cx: Scope) -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! { cx, <h1>"Not Found"</h1> }
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ cfg_if! {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <Counters/> }
|
||||
mount_to_body(|| {
|
||||
view! { <Counters/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ cfg_if! {
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
|
||||
let routes = generate_route_list(|| view! { <Counters/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
@@ -52,36 +52,15 @@ cfg_if! {
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
// serve JS/WASM/CSS from `pkg`
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
// serve other assets from the `assets` directory
|
||||
.service(Files::new("/assets", site_root))
|
||||
// serve the favicon from /favicon.ico
|
||||
.service(favicon)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
Counters,
|
||||
)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <Counters/> })
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
|
||||
) -> actix_web::Result<actix_files::NamedFile> {
|
||||
let leptos_options = leptos_options.into_inner();
|
||||
let site_root = &leptos_options.site_root;
|
||||
Ok(actix_files::NamedFile::open(format!(
|
||||
"{site_root}/favicon.ico"
|
||||
))?)
|
||||
}
|
||||
}
|
||||
|
||||
// client-only main for Trunk
|
||||
else {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5,13 +5,13 @@ use leptos_router::*;
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleQueryCounter(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_query_signal::<i32>(cx, "count");
|
||||
pub fn SimpleQueryCounter() -> impl IntoView {
|
||||
let (count, set_count) = create_query_signal::<i32>("count");
|
||||
let clear = move |_| set_count(None);
|
||||
let decrement = move |_| set_count(Some(count().unwrap_or(0) - 1));
|
||||
let increment = move |_| set_count(Some(count().unwrap_or(0) + 1));
|
||||
|
||||
let (msg, set_msg) = create_query_signal::<String>(cx, "message");
|
||||
let (msg, set_msg) = create_query_signal::<String>("message");
|
||||
let update_msg = move |ev| {
|
||||
let new_msg = event_target_value(&ev);
|
||||
if new_msg.is_empty() {
|
||||
@@ -21,7 +21,7 @@ pub fn SimpleQueryCounter(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
|
||||
@@ -5,8 +5,8 @@ use leptos_router::*;
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
mount_to_body(|| {
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view=SimpleQueryCounter />
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,45 +2,31 @@ use leptos::{ev, html::*, *};
|
||||
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, Count::new(initial_value, step));
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let count = 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(cx)
|
||||
div()
|
||||
// children can be added with .child()
|
||||
// this takes any type that implements IntoView as its argument
|
||||
// for example, a string or an HtmlElement<_>
|
||||
.child(
|
||||
button(cx)
|
||||
// it can also take an array of types that impl IntoView
|
||||
// or a tuple of up to 26 objects that impl IntoView
|
||||
.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()))
|
||||
.on(ev::click, move |_| count.update(Count::clear))
|
||||
.child("Clear"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
button()
|
||||
.on(ev::click, move |_| count.update(Count::decrease))
|
||||
.child("-1"),
|
||||
)
|
||||
.child(
|
||||
span(cx)
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child(move || count.get().value())
|
||||
.child("!"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| count.update(Count::increase))
|
||||
.child("+1"),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -5,5 +5,5 @@ use leptos::*;
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| counter(cx, 0, 1))
|
||||
mount_to_body(|| counter(0, 1))
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ fn should_clear_counter() {
|
||||
|
||||
fn open_counter() {
|
||||
remove_existing_counter();
|
||||
mount_to_body(move |cx| counter(cx, 0, 1));
|
||||
mount_to_body(move || counter(0, 1));
|
||||
}
|
||||
|
||||
fn remove_existing_counter() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -10,14 +10,14 @@ struct CounterUpdater {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
|
||||
provide_context(cx, CounterUpdater { set_counters });
|
||||
pub fn Counters() -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
|
||||
provide_context(CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id();
|
||||
let sig = create_signal(cx, 0);
|
||||
let sig = create_signal(0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
@@ -25,7 +25,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let signal = create_signal(0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
@@ -65,8 +65,8 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
<For
|
||||
each=counters
|
||||
key=|counter| counter.0
|
||||
view=move |cx, (id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! { cx,
|
||||
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! {
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
@@ -78,22 +78,29 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
cx: Scope,
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap();
|
||||
let CounterUpdater { set_counters } = use_context().unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
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(cx, || 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! { cx,
|
||||
view! {
|
||||
<li>
|
||||
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user