Compare commits

...

150 Commits

Author SHA1 Message Date
Greg Johnston
b80f9e3871 fix: issue with ordering of class attribute and class=("fancy-name-200", true) (closes #907) (#914) 2023-04-21 12:42:35 -04:00
Greg Johnston
328d42656d docs: compile error on mutually-exclusive features (#911) 2023-04-21 12:25:21 -04:00
Logan B. Nielsen
d3d2cbed7e feat: add typed window event listeners (#910) 2023-04-21 11:43:11 -04:00
agilarity
d6f7aedec1 CI: use cargo make to run tests for examples (#904) 2023-04-21 10:33:12 -04:00
Daniel Santana
7a5a776cb9 feat: get_untracked for node_ref. (#902) 2023-04-19 20:09:54 -04:00
Greg Johnston
06f782aa13 perf: improve router performance on server by calculating route branches once (#898) 2023-04-19 20:09:29 -04:00
Greg Johnston
6b825fec37 fix: erroneous non-reactive access warning in undelegated events (#900) 2023-04-19 20:09:05 -04:00
Greg Johnston
b452d8af40 feat: add ability to mutate resources (closes #856) (#886) 2023-04-19 11:40:46 -04:00
Daniel Santana
e96f1d2129 feat: impl Serialize/Deserialize for ParamsMap (closes #892) (#895) 2023-04-19 06:19:53 -04:00
OvermindDL1
72d6af9c84 fix: use once_cell crate until OnceLock stabilized (closes #890)
* Fixes #890 that was using OnceLock, which is nightly only, by adding the once_cell crate as a dependency.

* Make `cargo fmt` happy
2023-04-18 16:31:04 -04:00
Filip Dutescu
8198cd0b68 chore(readme): add link to Matrix bridge (#894)
While the project offers a [Matrix][matrix] bridge, it is nowhere shown.
One would need to join the [Discord][discord] server and search through
it to find it.

To make it easier to join through [Matrix][matrix], add a badge in the
project `README.md`.

[matrix]: https://matrix.org/
[discord]: https://discord.com/

Fixes: #893

Signed-off-by: Filip Dutescu <filip@hucksy.dev>
2023-04-18 15:30:00 -04:00
Greg Johnston
fe68b47ba2 perf: tiny optimization on primitive child values (#887) 2023-04-17 22:09:10 -04:00
Greg Johnston
384d39543c fix: dispose of scope when server fns return error (closes #862) (#888) 2023-04-17 22:08:47 -04:00
agilarity
225e62d12f examples: split counter without macros web test (#884) 2023-04-17 20:26:31 -04:00
Greg Johnston
3905a2aa60 docs: SSR modes 2023-04-17 17:00:52 -04:00
Greg Johnston
ff6ce2dac0 docs: add SSR mode videos 2023-04-17 16:03:36 -04:00
Greg Johnston
16675cbff2 docs: add chapter on styling 2023-04-17 11:50:50 -04:00
HJin.me
9524c6e289 fix: <For/> rendering error in SSR InOrder/Async Mode (#879) 2023-04-17 10:48:07 -04:00
Mark Catley
bc316c648c feat: add expect_context function (#864)
Most of the time when using use_context it would be a bug if the context
wasn't present and appropriate to panic. This is a convenience function
that has that behavior.
2023-04-17 10:47:50 -04:00
Matt Crane
6753ba21c4 fix: allow server functions to work with non-Cargo build systems with SERVER_FN_OVERRIDE_KEY env var (#878) 2023-04-17 08:46:32 -04:00
Greg Johnston
efbe32e081 feat: add non-animation base classes to <AnimatedOutlet/> and <AnimatedRoutes/> (#877) 2023-04-17 08:12:22 -04:00
Kamil Ogórek
55fd6d44f9 docs: Add per-project toolchain override readme (#876) 2023-04-16 16:30:20 -04:00
Mustafa Zaki Assagaf
90972f2d94 fix: updated nix flakes lock files on session auth axum examples to fix once_cell doesn't compile (#872) 2023-04-15 13:32:59 -04:00
Greg Johnston
7382c7e51c feat: add the ability to specify animations on route transitions (#736) 2023-04-14 18:20:42 -04:00
Greg Johnston
8a6d129575 examples: fix error handling in fetch example (#870) 2023-04-14 16:13:14 -04:00
Stackingttv
e20c77710d docs: fixed typo in life cycle docs (#869) 2023-04-14 15:12:18 -04:00
Greg Johnston
93da88eac0 feat: add ability to set node_ref and pass additional attributes to <Form/> and friends (#853) 2023-04-14 14:25:52 -04:00
agilarity
5072539917 examples: fix counter_without_macros test (#863) 2023-04-14 14:06:53 -04:00
Chris Roth
78c59df1d1 docs: fix match statement (#860) 2023-04-14 14:05:21 -04:00
Greg Johnston
75e40eafb2 docs: add "Life Cycle of a Page Load" 2023-04-14 13:30:53 -04:00
Álvaro Mondéjar
274a1ac5f0 Remove & at the end of params queries (#854) 2023-04-12 17:04:22 -04:00
Greg Johnston
17040a4af4 fix: custom events in SSR mode (#852) 2023-04-12 13:21:36 -04:00
Greg Johnston
b09a5f905e docs: emit error when trying to combine global class and dynamic class in a bugged way (#850) 2023-04-11 21:15:07 -04:00
Greg Johnston
683511f311 clippy 2023-04-11 14:37:54 -04:00
Greg Johnston
151c58733b docs: clean up methods documentation 2023-04-11 14:37:12 -04:00
Greg Johnston
012ff56cd6 fix static text nodes with curly braces in SSR (#849) 2023-04-11 12:46:32 -04:00
Nova
493c805993 feat: Trigger primitive and reactive-system cleanups (#838) 2023-04-10 17:47:52 -04:00
Greg Johnston
764192af36 feat: allow multiple HTTP request methods/verbs (#695) 2023-04-10 16:42:15 -04:00
Greg Johnston
f969fd7eff fix: don't entity-encode HTML special characters inside <script> or <style> (closes #837) (#846) 2023-04-10 13:15:15 -04:00
Greg Johnston
2c7ee0d415 feat: rustls feature for reqwest and any other relevant dependencies (#842) 2023-04-10 13:15:00 -04:00
Snêu
5430c78e18 docss: correct broken MaybeSignal link (#840) 2023-04-10 07:37:41 -04:00
Greg Johnston
6b052557d1 fix: correct todo_app_sqlite README (closes #845) 2023-04-10 07:17:46 -04:00
Nova
70f3edb0f5 fix: fix leaks in memos, and in scope parent tracking (#841) 2023-04-09 16:36:53 -04:00
Greg Johnston
4e1f963750 Merge pull request #831 from novacrazy/main
Various optimizations, size reductions and stability improvements
2023-04-08 09:04:13 -04:00
novacrazy
3c3d3b33f1 Remove unnecessary into 2023-04-07 17:41:27 -05:00
novacrazy
be7b9eea25 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-07 14:09:10 -05:00
Greg Johnston
016ad6b7a6 feat: make __Props imports unnecessary (closes #746) (#828) 2023-04-07 15:06:10 -04:00
Greg Johnston
5dab35447a update README.md 2023-04-07 13:19:35 -04:00
Greg Johnston
63be819533 tests: update benchmarks (#827)
* tests: add Criterion benchmarking and move reactive benchmarks into `leptos_reactive`
* tests: updated SSR benchmarks
2023-04-07 13:04:26 -04:00
Aaron Karras
af8afb1204 perf: use local pools for axum handlers (#815) 2023-04-07 11:35:16 -04:00
Mark Catley
2170be8e01 chore: deny warnings on github actions (#814)
Enabling on all except for checking examples to start. I'll fix those
and add it as a follow up.

Closes #795
2023-04-07 09:28:48 -04:00
Greg Johnston
1187a506dd fix: server functions with url as argument name (closes issue #823) (#825) 2023-04-07 09:28:31 -04:00
Greg Johnston
ff5ceddbe2 fix: correctly pass server fn errors to client (#822) 2023-04-07 08:12:10 -04:00
Greg Johnston
41a5e09caa docs: add sandbox links and max height (#824) 2023-04-07 07:38:12 -04:00
novacrazy
60b96c9118 Couple more inline tweaks 2023-04-07 05:28:50 -05:00
novacrazy
7ccb2d9f44 Simplify SsrMode enum 2023-04-07 05:10:55 -05:00
novacrazy
2c2090a194 Return Cow from as_value_string 2023-04-07 05:09:49 -05:00
novacrazy
de9b2998ac More inlining 2023-04-07 05:09:24 -05:00
novacrazy
29b81a3d50 Another round of inlining 2023-04-07 01:44:18 -05:00
novacrazy
5bc0d89ce7 Cleanup Comment::new 2023-04-07 00:52:35 -05:00
novacrazy
342b10c232 Use Cow for ErrorKey 2023-04-07 00:52:23 -05:00
novacrazy
ba9d3c1602 Another round of inlining 2023-04-07 00:52:11 -05:00
novacrazy
d3b3ce6980 Cleanup benchmarks 2023-04-06 21:56:24 -05:00
novacrazy
4b79a91287 Add profile.release to many examples 2023-04-06 21:53:52 -05:00
novacrazy
de06c9b2ca Fix Box<dyn> casts 2023-04-06 21:52:25 -05:00
novacrazy
84c7d00ea9 Use Cow<'static, str> for Attributes 2023-04-06 21:52:11 -05:00
novacrazy
8f5ae0054d Second round of inlining 2023-04-06 21:39:29 -05:00
Nova
374f0c4e27 Merge branch 'leptos-rs:main' into main 2023-04-06 21:31:41 -05:00
novacrazy
a6170f4da9 First round of inlining 2023-04-06 21:02:40 -05:00
novacrazy
578dd5ef35 Convert bubbles to associated const for more reliable const-eval and dead-code elimination 2023-04-06 20:55:18 -05:00
novacrazy
934a131deb Pull out non-generic code from leptos_dom
Avoids duplicate codegen
2023-04-06 20:52:13 -05:00
novacrazy
5bc1c36e67 Pull out non-generic code in reactive core 2023-04-06 20:32:59 -05:00
novacrazy
b1b9853f92 Replace with_scope_property with push_scope_property
Avoids duplicate codegen
2023-04-06 20:26:58 -05:00
novacrazy
5d6a083d1d Fix nested batching and improve batch/untrack behavior 2023-04-06 20:22:58 -05:00
Bram
9478245986 docs: remove Leptos guide link (same as book?) (#818) 2023-04-06 20:44:26 -04:00
Bram
4c1c12734a docs: publish book during CI (#817) 2023-04-06 14:09:54 -04:00
Greg Johnston
5d3a360456 fix: correctly escape HTML special characters in text nodes during SSR (#812) 2023-04-06 06:52:59 -04:00
novacrazy
b51da35a9a Remove allocation in ScopeDisposer 2023-04-05 21:21:22 -05:00
novacrazy
164dcd1b97 Cleanup signal clone impl 2023-04-05 21:19:04 -05:00
novacrazy
c0964c2b01 Fast path for linear deeply nested children 2023-04-05 21:17:37 -05:00
novacrazy
af5b226e53 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-05 19:30:06 -05:00
Nova
4e7a0db950 perf: optimize memory usage of update methods (#809) 2023-04-05 20:16:53 -04:00
Nova
cee6ed9a9f perf: optimize Runtime::mark_dirty (#808) 2023-04-05 20:16:40 -04:00
Greg Johnston
fa1013f7c3 chore: fix unused variable warning in property now that it's not memoized (#810) 2023-04-05 13:20:16 -04:00
novacrazy
3a1db3a191 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-05 08:39:49 -05:00
Ben Wishovich
8b57ba7aa8 feat: add the ability for server fns to be submitted via GET requests (#789) 2023-04-05 06:47:17 -04:00
Mark Catley
ea638e37f6 fix: unused warning in reactive signal diagnostics (#807) 2023-04-05 06:26:36 -04:00
Nova
4342d45a2f perf: optimize size of RuntimeId when slotmap is not used (#805) 2023-04-05 06:26:17 -04:00
Greg Johnston
fe4d2382b8 fix: prevent router panic on root-level <Redirect/> during route list generation (#801) 2023-04-04 21:36:03 -04:00
Greg Johnston
2a13609eff fix: fixes #802 as a temporary measure without resorting to #803 yet (#804) 2023-04-04 20:50:50 -04:00
Marcus Ofenhed
c2ff1cabf1 feat: Add ability to include options to event listeners (#799) 2023-04-04 20:50:35 -04:00
novacrazy
54370e3153 Reduce size of RuntimeId when slotmap is not used 2023-04-04 19:26:57 -05:00
Mark Catley
e72ed26809 fix: warning in Cargo.toml (#800) 2023-04-04 19:53:05 -04:00
Greg Johnston
64e056ffa9 docs: warn if you are using leptos_meta without features (#797) 2023-04-03 21:07:43 -04:00
Mark Catley
db9b7db53d fix: unused warning on cx in server functions (#794)
When running cargo clippy on server functions that use `cx: Scope` it
has an unused variable error.

It appears that the logic for adding an `#[allow(unused)]` notation is
inverted.
2023-04-03 21:07:30 -04:00
ealmloff
a9e6590b5e fix: server functions with non-copy server contexts (#785) 2023-04-03 07:17:22 -04:00
Greg Johnston
b67121b755 docs: <Form/> component (#792) 2023-04-02 16:50:21 -04:00
Greg Johnston
7bce4de682 fix: issues with nested <Suspense/> (closes #764) (#781) 2023-04-02 15:57:43 -04:00
Greg Johnston
8bdb427133 fix: improvements "untracked read" warnings in untrack, SSR cases (#791) 2023-04-02 15:57:06 -04:00
Patrick Auernig
4c23f3c478 chore: remove unused fs dependency from leptos_config (#787) 2023-04-02 12:29:30 -04:00
Greg Johnston
9502de561b fix: warnings about untracked signal access in <Router/> (#790) 2023-04-02 12:28:58 -04:00
Greg Johnston
210c11a733 docs: add runtime "strict mode" checks that warn if you’re non-reactively accessing a value (#786) 2023-04-01 17:41:25 -04:00
ealmloff
6917027204 fix server functions default macro on stable (#784) 2023-04-01 17:31:56 -04:00
Greg Johnston
e78ce7e6b9 feat: create_blocking_resource (#752) 2023-04-01 11:25:00 -04:00
Greg Johnston
a3327f8841 fix: SVG <title> tag (#783) 2023-04-01 11:24:32 -04:00
Greg Johnston
f727dd773b v0.2.5 (#782) 2023-04-01 11:23:42 -04:00
Greg Johnston
952646f066 Merge pull request #780 from leptos-rs/warn-on-routes-issues
docs: warn if you put something invalid inside `<Routes/>`
2023-03-31 17:13:02 -04:00
Greg Johnston
1e037ecb60 chore: clippy and docs warnings (#779) 2023-03-31 17:12:42 -04:00
Greg Johnston
c9f75d82d6 docs: warn if you add something that's not a <Route/> inside <Routes/> 2023-03-31 16:39:06 -04:00
Greg Johnston
de3849c20c example: show how to refactor routes into another component 2023-03-31 16:38:49 -04:00
Christian Rausch
c391c2e938 feat: arbitrary attributes to <Html/> and <Body/> meta tags (#726) 2023-03-31 16:30:10 -04:00
luoxiaozero
1cde4b1f8a docs: fixed parentheses and formatting issues (#775) 2023-03-31 15:48:29 -04:00
Greg Johnston
42360d109b change: insert <head> metadata tags at the beginning of the head, not the end (#731) 2023-03-31 14:51:27 -04:00
Kaszanas
7aa4d9e6db feat: Added `<ProtectedRoute/> component to route file (#741) 2023-03-31 14:50:46 -04:00
Kaszanas
9ed3390b81 examples: updated proxy settings in login_with_token_csr_only (#771)
When testing this example on Windows OS the initial value of `0.0.0.0:3000` for the IP did not work.
2023-03-31 14:44:06 -04:00
Greg Johnston
1ff56f7bfd fix: stop memoizing properties in a way that breaks prop:value (closes #768) (#772) 2023-03-30 19:44:38 -04:00
Greg Johnston
16917997cd fix: prevent forms from entering infinite loops (closes issue #760) (#762) 2023-03-30 16:28:49 -04:00
Greg Johnston
f42568d262 fix: <Redirect/> between nested routes at same level (#767) 2023-03-30 16:28:32 -04:00
Houski
97bbdf561a feat: added the id attribute to the Leptos router <A/> tag (#770) 2023-03-30 16:28:08 -04:00
Greg Johnston
f4043cbd9f fix: escape </script> and other HTML tags in serialized resources (#763) 2023-03-29 13:51:48 -04:00
Lukas Potthast
e9ff26abb4 feat: allow component declaration without use leptos::Scope in scope (#748) 2023-03-29 07:59:08 -04:00
Ben Wishovich
e6b1298915 feat: add property field to Meta component (#759) 2023-03-28 09:10:00 -04:00
Igor Shevchenko
98a9ec8335 chore(docs): fix a few typos (#756) 2023-03-27 20:06:34 -04:00
jquesada2016
5329561687 feat: add is_mounted and dyn_classes (#714) 2023-03-27 19:03:59 -04:00
Greg Johnston
89ca047f2f examples: improve counter_without_macros (#751) 2023-03-27 12:50:01 -04:00
Greg Johnston
a94711fcf0 fix: correct typecast on Memo::get_untracked (closes issue #754) (#755) 2023-03-27 11:28:40 -04:00
Greg Johnston
97d88c65ae docs: warn when reading resource outside <Suspense/> (closes issue #742) (#743) 2023-03-25 14:22:22 -04:00
Jessie Chatham Spencer
e482e3748d docs: document cargo workspace feature resolver footgun (#745)
Due to no rust edition being present in a workspac's Cargo.toml, non
WASM compatible code can end up being built for a WASM target.

This commit documents this error and how to resolve it.
2023-03-25 07:34:28 -04:00
István Donkó
8ab9c08448 docs: fix typo in server_fn docs (#740) 2023-03-24 21:42:27 -04:00
Lachlan Wilger
56de70b714 docs: fix typo (#739)
There was a typo in the section of the docs that pointed towards the hackernews example, so I fixed it by add the word "application."
2023-03-24 21:41:59 -04:00
Greg Johnston
38d97babd8 fix: always run dynamic classes after static classes (closes #735) (#738) 2023-03-24 17:38:34 -04:00
martin frances
4cfecb5d82 chore: bump serde-lite from 0.3 to 0.4. (#737) 2023-03-24 16:54:20 -04:00
Michael Clayton
08b5970b2b check EventSource value for Ok to avoid unwrap panic (#732) 2023-03-23 18:41:18 -04:00
Greg Johnston
af20f80b2b docs: fix typo in router docs (#730) 2023-03-22 20:44:58 -04:00
Andrew Chang-DeWitt
c2fdd2cd70 fix: include missing query params in navigation when <ActionForm/> receives a redirect (#728)
Previous solution in #727 included manually inserted `?` when a leading
`?` is present automatically in `Url.search`.
2023-03-22 20:05:21 -04:00
Greg Johnston
286f3eebe4 fix: relative routing should update when navigating between <Outlet/>s (closes issue #725) (#729)
* clear some cruft out of the navigation code
* fix issue #725 (correctly reactively resolving paths)
2023-03-22 19:59:08 -04:00
Álvaro Mondéjar
509223ab2e chore: Upgrade console_log to stable (#724) 2023-03-22 18:21:53 -04:00
Greg Johnston
665b0b8ed2 chore: make wasm-bindgen dependency optional in leptos_reactive (#723) 2023-03-22 17:56:52 -04:00
Greg Johnston
508ad52582 chore: fix clippy warnings (#721)
* `v0.2.4`

* chore: fix clippy warnings
2023-03-21 18:20:29 -04:00
martin frances
cfd5c98f97 clippy: simplify Box::pin() call. (#718) 2023-03-21 09:06:31 -04:00
Greg Johnston
2e63bb1f50 fix: <Transition/> behavior (#717) 2023-03-21 09:05:41 -04:00
Greg Johnston
982c8f6b5a docs: small fixes (#715) 2023-03-20 20:43:04 -04:00
Greg Johnston
12c4c115f3 docs: make is_odd less pretentious 2023-03-20 20:42:46 -04:00
Carlton Gibson
d4d20ecdb0 Used modulo rather than bitwise & for is_odd check.
The modulo operator is less of a head-scratcher for folks coming through here. The bitwise & is equally correct (clearly) but is likely to cause confusion if folks don't immediately see what's going on.
2023-03-20 20:09:02 +01:00
Greg Johnston
b78919c6ed Merge pull request #712 from leptos-rs/warnings 2023-03-20 10:49:00 -04:00
Greg Johnston
abb9320e31 chore: clear warning and add exports of helpers with handles 2023-03-20 09:36:14 -04:00
Greg Johnston
875d2d5a3a chore: handle unbounded_send warnings 2023-03-20 09:33:58 -04:00
Greg Johnston
42a58855a0 feat: add Scope::batch() (#711) 2023-03-20 08:29:18 -04:00
Greg Johnston
9d142758ec feat: allow manual signal disposal before the scope is disposed (#710) 2023-03-19 21:40:16 -04:00
Greg Johnston
2faddd85cb feat: add set_interval_with_handle and deprecate set_interval (#709) 2023-03-19 16:45:22 -04:00
206 changed files with 7312 additions and 2215 deletions

View File

@@ -42,4 +42,4 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make check-stable
run: cargo make --profile=github-actions check-stable

View File

@@ -42,4 +42,4 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all libraries
run: cargo make check
run: cargo make --profile=github-actions check

37
.github/workflows/publish-book.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Deploy book
on:
push:
paths: ['docs/book/**']
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write # To push a branch
pull-requests: write # To create a PR from that branch
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install mdbook
run: |
mkdir mdbook
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.27/mdbook-v0.4.27-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
- name: Deploy GitHub Pages
run: |
cd docs/book
mdbook build
git worktree add gh-pages
git config user.name "Deploy book from CI"
git config user.email ""
cd gh-pages
# Delete the ref to avoid keeping history.
git update-ref -d refs/heads/gh-pages
rm -rf *
mv ../book/* .
git add .
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
git push --force --set-upstream origin gh-pages

View File

@@ -42,4 +42,4 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Run tests with all features
run: cargo make test
run: cargo make --profile=github-actions test

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.2.3"
version = "0.2.5"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.2.3" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.3" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.3" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.3" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.3" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.3" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.3" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.3" }
leptos_router = { path = "./router", version = "0.2.3" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.3" }
leptos = { path = "./leptos", default-features = false, version = "0.2.5" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.5" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.5" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.5" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.5" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.5" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.5" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.5" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
leptos_router = { path = "./router", version = "0.2.5" }
leptos_meta = { path = "./meta", default-features = false, version = "0.2.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
[profile.release]
codegen-units = 1

View File

@@ -74,3 +74,21 @@ dependencies = ["test-all"]
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
[tasks.test-examples]
description = "Run all unit and web tests for examples"
cwd = "examples"
command = "cargo"
args = ["make", "test-unit-and-web"]
[tasks.verify-examples]
description = "Run all quality checks and tests for examples"
cwd = "examples"
command = "cargo"
args = ["make", "verify-flow"]
[env]
RUSTFLAGS = ""
[env.github-actions]
RUSTFLAGS = "-D warnings"

View File

@@ -6,6 +6,7 @@
[![crates.io](https://img.shields.io/crates/v/leptos.svg)](https://crates.io/crates/leptos)
[![docs.rs](https://docs.rs/leptos/badge.svg)](https://docs.rs/leptos)
[![Discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord)](https://discord.gg/YdRAhS7eQB)
[![Matrix](https://img.shields.io/badge/Matrix-leptos-grey?logo=matrix&labelColor=white&logoColor=black)](https://matrix.to/#/#leptos:matrix.org)
# Leptos
@@ -24,8 +25,7 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
let increment = move |_| set_value.update(|value| *value += 1);
// create user interfaces with the declarative `view!` macro
view! {
cx,
view! { cx,
<div>
<button on:click=clear>"Clear"</button>
<button on:click=decrement>"-1"</button>
@@ -48,27 +48,27 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
## What does that mean?
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
## Learn more
Here are some resources for learning more about Leptos:
- [Book](https://leptos-rs.github.io/leptos/) (work in progress)
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
- Leptos Guide (in progress)
## `nightly` Note
Most of the examples assume youre using `nightly` Rust.
Most of the examples assume youre using `nightly` version of Rust. For this, you can either set your toolchain globally or on per-project basis.
To set up your Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you havent already)
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you havent already):
```
rustup toolchain install nightly
@@ -76,6 +76,14 @@ rustup default nightly
rustup target add wasm32-unknown-unknown
```
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
```toml
[toolchain]
channel = "nightly"
targets = ["wasm32-unknown-unknown"]
```
If youre on `stable`, note the following:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
@@ -86,7 +94,7 @@ If youre on `stable`, note the following:
## `cargo-leptos`
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
```bash
cargo install cargo-leptos
@@ -95,13 +103,13 @@ cd [your project name]
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
Open browser to [http://localhost:3000/](http://localhost:3000/).
## FAQs
### Whats up with the name?
*Leptos* (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
### Is it production ready?
@@ -109,7 +117,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?
With 0.1 the APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
The APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
2. **Are there bugs?**
@@ -119,7 +127,7 @@ Yes, Im sure there are. You can see from the state of our issue tracker over
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) dont have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if youre willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if youre willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
### Can I use this for native GUI?
@@ -137,8 +145,8 @@ I've put together a [very simple GTK example](https://github.com/leptos-rs/lepto
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. (Dioxus has made huge advances in performance with its recent 0.3 release, and is now roughly on par with Leptos.)
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. You can call functions, create timeouts, etc. within the body of your component functions because they wont be re-run. You dont need to think about manual dependency tracking for effects; fine-grained reactivity tracks dependencies automatically.
### How is this different from Sycamore?
@@ -146,9 +154,9 @@ Conceptually, these two frameworks are very similar: because both are built on f
There are some practical differences that make a significant difference:
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
```rust

View File

@@ -17,15 +17,11 @@ lazy_static = "1"
log = "0.4"
strum = "0.24"
strum_macros = "0.24"
serde = { version = "1", features = ["derive", "rc"]}
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
tera = "1"
reactive-signals = "0.1.0-alpha.4"
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
"Document",
"HtmlElement",
"HtmlInputElement"
]
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]

View File

@@ -2,6 +2,6 @@
extern crate test;
mod reactive;
//åmod reactive;
//mod ssr;
//mod todomvc;
mod todomvc;

View File

@@ -10,8 +10,8 @@ fn leptos_deep_creation(b: &mut Bencher) {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
@@ -34,9 +34,8 @@ fn leptos_deep_update(b: &mut Bencher) {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
@@ -162,6 +161,77 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
runtime.dispose();
}
#[bench]
fn rs_deep_update(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let sc = ClientRuntime::new_root_scope();
b.iter(|| {
let signal = signal!(sc, 0);
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(signal!(sc, move || prev.get() + 1))
} else {
memos.push(signal!(sc, move || signal.get() + 1))
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
});
}
#[bench]
fn rs_fanning_out(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let sig = signal!(cx, 0);
let memos = (0..1000)
.map(|_| signal!(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
});
}
#[bench]
fn rs_narrowing_update(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let memo = signal!(cx, {
let sigs = sigs.clone();
move || {
sigs.iter().map(|r| r.get()).sum::<i32>()
}
});
assert_eq!(memo.get(), 499500);
signal!(cx, {
let acc = Rc::clone(&acc);
move || {
acc.set(memo.get());
}
});
assert_eq!(acc.get(), 499500);
sigs[1].update(|n| *n += 1);
sigs[10].update(|n| *n += 1);
sigs[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo.get(), 499503);
});
}
#[bench]
fn l021_deep_creation(b: &mut Bencher) {
use l021::*;
@@ -171,9 +241,8 @@ fn l021_deep_creation(b: &mut Bencher) {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
@@ -195,9 +264,8 @@ fn l021_deep_update(b: &mut Bencher) {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
@@ -373,9 +441,8 @@ fn sycamore_deep_creation(b: &mut Bencher) {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));
@@ -394,9 +461,8 @@ fn sycamore_deep_update(b: &mut Bencher) {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));

View File

@@ -4,7 +4,7 @@ use test::Bencher;
fn leptos_ssr_bench(b: &mut Bencher) {
b.iter(|| {
use leptos::*;
HydrationCtx::reset_id();
leptos_dom::HydrationCtx::reset_id();
_ = create_scope(create_runtime(), |cx| {
#[component]
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
@@ -32,7 +32,8 @@ fn leptos_ssr_bench(b: &mut Bencher) {
assert_eq!(
rendered,
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here&#x27;s some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>"
);
});
});
}

View File

@@ -1,6 +1,7 @@
pub use leptos::*;
use miniserde::*;
use web_sys::HtmlInputElement;
use wasm_bindgen::JsCast;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Todos(pub Vec<Todo>);
@@ -110,10 +111,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
provide_context(cx, set_todos);
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener("hashchange", move |_| {
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
});
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
@@ -167,57 +164,79 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
});
view! { cx,
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" on:keydown=add_todo />
</header>
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
<input id="toggle-all" class="toggle-all" type="checkbox"
prop:checked={move || todos.with(|t| t.remaining() > 0)}
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
<For
each=filtered_todos
key=|todo| todo.id
view=move |todo: Todo| view! { cx, <Todo todo=todo.clone() /> }
/>
</ul>
</section>
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 {
" item"
} else {
" items"
}}
" left"
</span>
<ul class="filters">
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
</ul>
<button
class="clear-completed hidden"
class:hidden={move || todos.with(|t| t.completed() == 0)}
on:click=move |_| set_todos.update(|t| t.clear_completed())
>
"Clear completed"
</button>
</footer>
</section>
<footer class="info">
<p>"Double-click to edit a todo"</p>
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}.into_view(cx)
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus=""
on:keydown=add_todo
/>
</header>
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
prop:checked=move || todos.with(|t| t.remaining() > 0)
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
<For
each=filtered_todos
key=|todo| todo.id
view=move |cx, todo: Todo| {
view! { cx, <Todo todo=todo.clone()/> }
}
/>
</ul>
</section>
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
" left"
</span>
<ul class="filters">
<li>
<a
href="#/"
class="selected"
class:selected=move || mode() == Mode::All
>
"All"
</a>
</li>
<li>
<a href="#/active" class:selected=move || mode() == Mode::Active>
"Active"
</a>
</li>
<li>
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
"Completed"
</a>
</li>
</ul>
<button
class="clear-completed hidden"
class:hidden=move || todos.with(|t| t.completed() == 0)
on:click=move |_| set_todos.update(|t| t.clear_completed())
>
"Clear completed"
</button>
</footer>
</section>
<footer class="info">
<p>"Double-click to edit a todo"</p>
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}.into_view(cx)
}
#[component]
@@ -237,41 +256,36 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
};
view! { cx,
<li
class="todo"
class:editing={editing}
class:completed={move || (todo.completed)()}
//_ref=input
>
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
<div class="view">
<input
class="toggle"
type="checkbox"
prop:checked={move || (todo.completed)()}
/>
<label on:dblclick=move |_| set_editing(true)>
{move || todo.title.get()}
</label>
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
<button
class="destroy"
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
></button>
</div>
{move || editing().then(|| view! { cx,
<input
class="edit"
class:hidden={move || !(editing)()}
prop:value={move || todo.title.get()}
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup={move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {
set_editing(false);
{move || {
editing()
.then(|| {
view! { cx,
<input
class="edit"
class:hidden=move || !(editing)()
prop:value=move || todo.title.get()
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup=move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {
set_editing(false);
}
}
/>
}
}}
/>
})
}
})
}}
</li>
}
}

View File

@@ -7,19 +7,15 @@ mod yew;
#[bench]
fn leptos_todomvc_ssr(b: &mut Bencher) {
use ::leptos::*;
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::leptos::*;
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new(cx)/>
}
.into_view(cx)
.render_to_string(cx);
assert!(rendered.len() > 1);
let html = ::leptos::ssr::render_to_string(|cx| {
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
});
assert!(html.len() > 1);
});
}
@@ -57,21 +53,20 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
});
});
}
/*
#[bench]
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
b.iter(|| {
use self::leptos::*;
use ::leptos::*;
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
let html = ::leptos::ssr::render_to_string(|cx| {
view! {
cx,
<TodoMVC todos=Todos::new_with_1000(cx)/>
}.into_view(cx).render_to_string(cx);
assert!(rendered.len() > 1);
}
});
assert!(html.len() > 1);
});
}
@@ -108,5 +103,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
assert!(rendered.len() > 1);
});
});
}
*/
}

View File

@@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) {
let _ = TERA.render("template.html", &ctx).unwrap();
});
}
}

View File

@@ -61,3 +61,19 @@ view! {
<input prop:value=a on:input=on_input />
}
```
## Build configuration
### Cargo feature resolution in workspaces
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
```toml
[workspace]
members = ["member1", "member2"]
resolver = "2"
```

View File

@@ -107,4 +107,6 @@ create_effect(cx, move |prev_value| {
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -84,7 +84,7 @@ fn FancyMath(cx: Scope) -> impl IntoView {
This kind of “provide a signal in a parent, consume it in a child” should be familiar
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
pattern you use to communicate between parents and children works for grandparents and
grandchildren, or any ancestors and descendents: in other words, between “global” state
grandchildren, or any ancestors and descendants: in other words, between “global” state
in the root component of your app and any other components anywhere else in the app.
Because of the fine-grained nature of updates, this is usually all you need. However,
@@ -122,6 +122,7 @@ fn App(cx: Scope) -> impl IntoView {
provide_context(cx, state);
// ...
}
```
Then child components can access “slices” of that state with fine-grained
@@ -168,4 +169,6 @@ somewhere else that only takes `state.name`, clicking the button wont cause
that other slice to update. This allows you to combine the benefits of a top-down
data flow and of fine-grained reactive updates.
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px">
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh">

View File

@@ -26,14 +26,18 @@
- [Nested Routing](./router/17_nested_routing.md)
- [Params and Queries](./router/18_params_and_queries.md)
- [`<A/>`](./router/19_a.md)
- [`<Form/>`]()
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [`<Form/>`](./router/20_form.md)
- [Interlude: Styling](./interlude_styling.md)
- [Metadata]()
- [SSR]()
- [Models of SSR]()
- [Server Side Rendering](./ssr/README.md)
- [`cargo-leptos`]()
- [The Life of a Page Load](./ssr/21_life_cycle.md)
- [Async Rendering and SSR “Modes”](./ssr/22_ssr_modes.md)
- [Hydration Footguns]()
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
- [Headers]()
- [Cookies]()
- [Server Functions]()
@@ -42,3 +46,4 @@
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size]()

View File

@@ -50,4 +50,6 @@ view! { cx,
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -26,11 +26,11 @@ let b = create_resource(cx, count2, |count| async move { load_b(count).await });
view! { cx,
<h1>"My Data"</h1>
{move || match (a.read(cx), b.read(cx)) {
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
(Some(a), Some(b)) => view! { cx,
<ShowA a/>
<ShowA b/>
}.into_view(cx)
}.into_view(cx),
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
}}
}
```
@@ -69,4 +69,6 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
This inversion of the flow of control makes it easier to add or remove individual resources, as you dont need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which well talk about during a later chapter.
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -4,6 +4,8 @@ Youll notice in the `<Suspense/>` example that if you keep reloading the data
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. This can be a much better user experience than constantly falling back to a loading message.
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -91,4 +91,6 @@ view! { cx,
Now, theres a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, youll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Dont worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,112 @@
# Interlude: Styling
Anyone creating a website or application soon runs into the question of styling. For a small app, a single CSS file is probably plenty to style your user interface. But as an application grows, many developers find that plain CSS becomes increasingly hard to manage.
Some frontend frameworks (like Angular, Vue, and Svelte) provide built-in ways to scope your CSS to particular components, making it easier to manage styles across a whole application without styles meant to modify one small component having a global effect. Other frameworks (like React or Solid) dont provide built-in CSS scoping, but rely on libraries in the ecosystem to do it for them. Leptos is in this latter camp: the framework itself has no opinions about CSS at all, but provides a few tools and primitives that allow others to build styling libraries.
Here are a few different approaches to styling your Leptos app, other than plain CSS.
## TailwindCSS: Utility-first CSS
[TailwindCSS](https://tailwindcss.com/) is a popular utility-first CSS library. It allows you to style your application by using inline utility classes, with a custom CLI tool that scans your files for Tailwind class names and bundles the necessary CSS.
This allows you to write components like this:
```rust
#[component]
fn Home(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<main class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
<button
class="bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
on:click=move |_| set_count.update(|count| *count += 1)
>
{move || if count() == 0 {
"Click me!".to_string()
} else {
count().to_string()
}}
</button>
</main>
}
}
```
It can be a little complicated to set up the Tailwind integration at first, but you can check out our two examples of how to use Tailwind with a [client-side-rendered `trunk` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr_trunk) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind). `cargo-leptos` also has some [built-in Tailwind support](https://github.com/leptos-rs/cargo-leptos#site-parameters) that you can use as an alternative to Tailwinds CLI.
## Stylers: Compile-time CSS Extraction
[Stylers](https://github.com/abishekatp/stylers) is a compile-time scoped CSS library that lets you declare scoped CSS in the body of your component. Stylers will extract this CSS at compile time into CSS files that you can then import into your app, which means that it doesnt add anything to the WASM binary size of your application.
This allows you to write components like this:
```rust
use stylers::style;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let styler_class = style! { "App",
#two{
color: blue;
}
div.one{
color: red;
content: raw_str(r#"\hello"#);
font: "1.3em/1.2" Arial, Helvetica, sans-serif;
}
div {
border: 1px solid black;
margin: 25px 50px 75px 100px;
background-color: lightblue;
}
h2 {
color: purple;
}
@media only screen and (max-width: 1000px) {
h3 {
background-color: lightblue;
color: blue
}
}
};
view! { cx, class = styler_class,
<div class="one">
<h1 id="two">"Hello"</h1>
<h2>"World"</h2>
<h2>"and"</h2>
<h3>"friends!"</h3>
</div>
}
}
```
## Styled: Runtime CSS Scoping
[Styled](https://github.com/eboody/styled) is a runtime scoped CSS library that integrates well with Leptos. It lets you declare scoped CSS in the body of your component function, and then applies those styles at runtime.
```rust
use styled::style;
#[component]
pub fn MyComponent(cx: Scope) -> impl IntoView {
let styles = style!(
div {
background-color: red;
color: white;
}
);
styled::view! { cx, styles,
<div>"This text should be red with white text."</div>
}
}
```
## Contributions Welcome
Leptos no opinions on how you style your website or app, but were very happy to provide support to any tools youre trying to create to make it easier. If youre working on a CSS or styling approach that youd like to add to this list, please let us know!

View File

@@ -167,4 +167,6 @@ In fact, in this case, we dont even need to rerender the `<Contact/>` compone
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple well cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so dont be surprised if theres anything you dont understand.
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -58,8 +58,8 @@ let id = move || {
The untyped versions return `Memo<ParamsMap>`. Again, its 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::<ContactParams>(cx);
let query = use_query::<ContactSearch>(cx);
let params = use_params_map(cx);
let query = use_query_map(cx);
// id: || -> Option<String>
let id = move || {
@@ -74,4 +74,6 @@ This can get a little messy: deriving a signal that wraps an `Option<_>` or `Res
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we havent explain them all yet.
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -18,4 +18,6 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,69 @@
# The `<Form/>` Component
Links and forms sometimes seem completely unrelated. But in fact, they work in very similar ways.
In plain HTML, there are three ways to navigate to another page:
1. An `<a>` element that links to another page. Navigates to the URL in its `href` attribute with the `GET` HTTP method.
2. A `<form method="GET">`. Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
3. A `<form method="POST">`. Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
The router provides a [`<Form>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Form.html) component, which works like the HTML `<form>` element, but uses client-side navigations instead of full page reloads. `<Form/>` works with both `GET` and `POST` requests. With `method="GET"`, it will navigate to the URL encoded in the form data. With `method="POST"` it will make a `POST` request and handle the servers response.
`<Form/>` provides the basis for some components like `<ActionForm/>` and `<MultiActionForm/>` that well see in later chapters. But it also enables some powerful patterns of its own.
For example, imagine that you want to create a search field that updates search results in real time as the user searches, without a page reload, but that also stores the search in the URL so a user can copy and paste it to share results with someone else.
It turns out that the patterns weve learned so far make this easy to implement.
```rust
async fn fetch_results() {
// some async function to fetch our search results
}
#[component]
pub fn Search(cx: Scope) -> impl IntoView {
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
// reactive access to URL query strings
let query = use_query_map(cx);
// search stored as ?q=
let search = move || query().get("q").cloned().unwrap_or_default();
// a resource driven by the search string
let search_results = create_resource(cx, search, fetch_results);
view! { cx,
<Form method="GET" action="">
<input type="search" name="search" value=search/>
<input type="submit"/>
</Form>
<Transition fallback=move || ()>
/* render search results */
</Transition>
}
}
```
Whenever you click `Submit`, the `<Form/>` will “navigate” to `?q={search}`. But because this navigation is done on the client side, theres no page flicker or reload. The URL query string changes, which triggers `search` to update. Because `search` is the source signal for the `search_results` resource, this triggers `search_results` to reload its resource. The `<Transition/>` continues displaying the current search results until the new ones have loaded. When they are complete, it switches to displaying the new result.
This is a great pattern. The data flow is extremely clear: all data flows from the URL to the resource into the UI. The current state of the application is stored in the URL, which means you can refresh the page or text the link to a friend and it will show exactly what youre expecting. And once we introduce server rendering, this pattern will prove to be really fault-tolerant, too: because it uses a `<form>` element and URLs under the hood, it actually works really well without even loading your WASM on the client.
We can actually take it a step further and do something kind of clever:
```rust
view! { cx,
<Form method="GET" action="">
<input type="search" name="search" value=search
oninput="this.form.requestSubmit()"
/>
</Form>
}
```
Youll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the users input as they type.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -16,7 +16,7 @@ The Leptos Router works with the path and query (`/blog/search?q=Search`). Given
## The Philosophy
In most cases, the path should drive what is displayed on the page. From the users perspective, for most appliations, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
In most cases, the path should drive what is displayed on the page. From the users perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.

View File

@@ -0,0 +1,43 @@
# The Life of a Page Load
Before we get into the weeds it might be helpful to have a higher-level overview. What exactly happens between the moment you type in the URL of a server-rendered Leptos app, and the moment you click a button and a counter increases?
Im assuming some basic knowledge of how the Internet works here, and wont get into the weeds about HTTP or whatever. Instead, Ill try to show how different parts of the Leptos APIs map onto each part of the process.
This description also starts from the premise that your app is being compiled for two separate targets:
1. A server version, often running on Actix or Axum, compiled with the Leptos `ssr` feature
2. A browser version, compiled to WebAssembly (WASM) with the Leptos `hydrate` feature
The [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool exists to coordinate the process of compiling your app for these two different targets.
## On the Server
- Your browser makes a `GET` request for that URL to your server. At this point, the browser knows almost nothing about the page thats going to be rendered. (The question “How does the browser know where to ask for the page?” is an interesting one, but out of the scope of this tutorial!)
- The server receives that request, and checks whether it has a way to handle a `GET` request at that path. This is what the `.leptos_routes()` methods in [`leptos_axum`](https://docs.rs/leptos_axum/0.2.5/leptos_axum/trait.LeptosRoutes.html) and [`leptos_actix`](https://docs.rs/leptos_actix/0.2.5/leptos_actix/trait.LeptosRoutes.html) are for. When the server starts up, these methods walk over the routing structure you provide in `<Routes/>`, generating a list of all possible routes your app can handle and telling the servers router “for each of these routes, if you get a request... hand it off to Leptos.”
- The server sees that this route can be handled by Leptos. So it renders your root component (often called something like `<App/>`), providing it with the URL thats being requested and some other data like the HTTP headers and request metadata.
- Your application runs once on the server, building up an HTML version of the component tree that will be rendered at that route. (Theres more to be said here about resources and `<Suspense/>` in the next chapter.)
- The server returns this HTML page, also injecting information on how to load the version of your app that has been compiled to WASM so that it can run in the browser.
> The HTML page thats returned is essentially your app, “dehydrated” or “freeze-dried”: it is HTML without any of the reactivity or event listeners youve added. The browser will “rehydrate” this HTML page by adding the reactive system and attaching event listeners to that server-rendered HTML. Hence the two feature flags that apply to the two halves of this process: `ssr` on the server for “server-side rendering”, and `hydrate` in the browser for that process of rehydration.
## In the Browser
- The browser receives this HTML page from the server. It immediately goes back to the server to begin loading the JS and WASM necessary to run the interactive, client side version of the app.
- In the meantime, it renders the HTML version.
- When the WASM version has reloaded, it does the same route-matching process that the server did. Because the `<Routes/>` component is identical on the server and in the client, the browser version will read the URL and render the same page that was already returned by the server.
- During this initial “hydration” phase, the WASM version of your app doesnt re-create the DOM nodes that make up your application. Instead, it walks over the existing HTML tree, “picking up” existing elements and adding the necessary interactivity.
> Note that there are some trade-offs here. Before this hydration process is complete, the page will _appear_ interactive but wont actually respond to interactions. For example, if you have a counter button and click it before WASM has loaded, the count will not increment, because the necessary event listeners and reactivity have not been added yet. Well look at some ways to build in “graceful degradation” in future chapters.
## Client-Side Navigation
The next step is very important. Imagine that the user now clicks a link to navigate to another page in your application.
The browser will _not_ make another round trip to the server, reloading the full page as it would for navigating between plain HTML pages or an application that uses server rendering (for example with PHP) but without a client-side half.
Instead, the WASM version of your app will load the new page, right there in the browser, without requesting another page from the server. Essentially, your app upgrades itself from a server-loaded “multi-page app” into a browser-rendered “single-page app.” This yields the best of both worlds: a fast initial load time due to the server-rendered HTML, and fast secondary navigations because of the client-side routing.
Some of what will be described in the following chapters—like the interactions between server functions, resources, and `<Suspense/>`—may seem overly complicated. You might find yourself asking, “If my page is being rendered to HTML on the server, why cant I just `.await` this on the server? If I can just call library X in a server function, why cant I call it in my component?” The reason is pretty simple: to enable the upgrade from server rendering to client rendering, everything in your application must be able to run either on the server or in the browser.
This is not the only way to create a website or web framework, of course. But its the most common way, and we happen to think its quite a good way, to create the smoothest possible experience for your users.

View File

@@ -0,0 +1,122 @@
# Async Rendering and SSR “Modes”
Server-rendering a page that uses only synchronous data is pretty simple: You just walk down the component tree, rendering each element to an HTML string. But this is a pretty big caveat: it doesnt answer the question of what we should do with pages that includes asynchronous data, i.e., the sort of stuff that would be rendered under a `<Suspense/>` node on the client.
When a page loads async data that it needs to render, what should we do? Should we wait for all the async data to load, and then render everything at once? (Lets call this “async” rendering) Should we go all the way in the opposite direction, just sending the HTML we have immediately down to the client and letting the client load the resources and fill them in? (Lets call this “synchronous” rendering) Or is there some middle-ground solution that somehow beats them both? (Hint: There is.)
If youve ever listened to streaming music or watched a video online, Im sure you realize that HTTP supports streaming, allowing a single connection to send chunks of data one after another without waiting for the full content to load. You may not realize that browsers are also really good at rendering partial HTML pages. Taken together, this means that you can actually enhance your users experience by **streaming HTML**: and this is something that Leptos supports out of the box, with no configuration at all. And theres actually more than one way to stream HTML: you can stream the chunks of HTML that make up your page in order, like frames of a video, or you can stream them... well, out of order.
Let me say a little more about what I mean.
Leptos supports all four different of these different ways to render HTML that includes asynchronous data.
## Synchronous Rendering
1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `<Suspense/>`. Load data on the client using `create_local_resource`, replacing `fallback` once resources are loaded.
- _Pros_: App shell appears very quickly: great TTFB (time to first byte).
- _Cons_
- Resources load relatively slowly; you need to wait for JS + WASM to load before even making a request.
- No ability to include data from async resources in the `<title>` or other `<meta>` tags, hurting SEO and things like social media link previews.
If youre using server-side rendering, the synchronous mode is almost never what you actually want, from a performance perspective. This is because it misses out on an important optimization. If youre loading async resources during server rendering, you can actually begin loading the data on the server. Rather than waiting for the client to receive the HTML response, then loading its JS + WASM, _then_ realize it needs the resources and begin loading them, server rendering can actually begin loading the resources when the client first makes the response. In this sense, during server rendering an async resource is like a `Future` that begins loading on the server and resolves on the client. As long as the resources are actually serializable, this will always lead to a faster total load time.
> This is why [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) requires resources data to be serializable by default, and why you need to explicitly use [`create_local_resource`](https://docs.rs/leptos/latest/leptos/fn.create_local_resource.html) for any async data that is not serializable and should therefore only be loaded in the browser itself. Creating a local resource when you could create a serializable resource is always a deoptimization.
## Async Rendering
<video controls>
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/async.mov?raw=true" type="video/mp4">
</video>
2. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
- _Pros_: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
- _Cons_: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client. The page is totally blank until everything is loaded.
## In-Order Streaming
<video controls>
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/in-order.mov?raw=true" type="video/mp4">
</video>
3. **In-order streaming**: Walk through the component tree, rendering HTML until you hit a `<Suspense/>`. Send down all the HTML youve got so far as a chunk in the stream, wait for all the resources accessed under the `<Suspense/>` to load, then render it to HTML and keep walking until you hit another `<Suspense/>` or the end of the page.
- _Pros_: Rather than a blank screen, shows at least _something_ before the data are ready.
- _Cons_
- Loads the shell more slowly than synchronous rendering (or out-of-order streaming) because it needs to pause at every `<Suspense/>`.
- Unable to show fallback states for `<Suspense/>`.
- Cant begin hydration until the entire page has loaded, so earlier pieces of the page will not be interactive until the suspended chunks have loaded.
## Out-of-Order Streaming
<video controls>
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/out-of-order.mov?raw=true" type="video/mp4">
</video>
4. **Out-of-order streaming**: Like synchronous rendering, serve an HTML shell that includes `fallback` for any `<Suspense/>`. But load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `<Suspense/>` nodes, which is swapped in to replace the fallback.
- _Pros_: Combines the best of **synchronous** and **`async`**.
- Fast initial response/TTFB because it immediately sends the whole synchronous shell
- Fast total time because resources begin loading on the server.
- Able to show the fallback loading state and dynamically replace it, instead of showing blank sections for un-loaded data.
- _Cons_: Requires JavaScript to be enabled for suspended fragments to appear in correct order. (This small chunk of JS streamed down in a `<script>` tag alongside the `<template>` tag that contains the rendered `<Suspense/>` fragment, so it does not need to load any additional JS files.)
## Using SSR Modes
Because it offers the best blend of performance characteristics, Leptos defaults to out-of-order streaming. But its really simple to opt into these different modes. You do it by adding an `ssr` property onto one or more of your `<Route/>` components, like in the [`ssr_modes` example](https://github.com/leptos-rs/leptos/blob/main/examples/ssr_modes/src/app.rs).
```rust
<Routes>
// Well load the home page with out-of-order streaming and <Suspense/>
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
// We'll load the posts with async rendering, so they can set
// the title and metadata *after* loading the data
<Route
path="/post/:id"
view=|cx| view! { cx, <Post/> }
ssr=SsrMode::Async
/>
</Routes>
```
For a path that includes multiple nested routes, the most restrictive mode will be used: i.e., if even a single nested route asks for `async` rendering, the whole initial request will be rendered `async`. `async` is the most restricted requirement, followed by in-order, and then out-of-order. (This probably makes sense if you think about it for a few minutes.)
## Blocking Resources
Any Leptos versions later than `0.2.5` (i.e., git main and `0.3.x` or later) introduce a new resource primitive with `create_blocking_resource`. A blocking resource still loads asynchronously like any other `async`/`.await` in Rust; it doesnt block a server thread or anything. Instead, reading from a blocking resource under a `<Suspense/>` blocks the HTML _stream_ from returning anything, including its initial synchronous shell, until that `<Suspense/>` has resolved.
Now from a performance perspective, this is not ideal. None of the synchronous shell for your page will load until that resource is ready. However, rendering nothing means that you can do things like set the `<title>` or `<meta>` tags in your `<head>` in actual HTML. This sounds a lot like `async` rendering, but theres one big difference: if you have multiple `<Suspense/>` sections, you can block on _one_ of them but still render a placeholder and then stream in the other.
For example, think about a blog post. For SEO and for social sharing, I definitely want my blog posts title and metadata in the initial HTML `<head>`. But I really dont care whether comments have loaded yet or not; Id like to load those as lazily as possible.
With blocking resources, I can do something like this:
```rust
#[component]
pub fn BlogPost(cx: Scope) -> impl IntoView {
let post_data = create_blocking_resource(cx, /* load blog post */);
let comment_data = create_resource(cx, /* load blog post */);
view! { cx,
<Suspense fallback=|| ()>
{move || {
post_data.with(cx, |data| {
view! { cx,
<Title text=data.title/>
<Meta name="description" content=data.excerpt/>
<article>
/* render the post content */
</article>
}
})
}}
</Suspense>
<Suspense fallback=|| "Loading comments...">
/* render comment data here */
</Suspense>
}
}
```
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.

View File

@@ -0,0 +1,21 @@
# Server Side Rendering
So far, everything weve written has been rendered almost entirely in the browser. When we create an app using Trunk, its served using a local development server. If you build it for production and deploy it, its served by whatever server or CDN youre using. In either case, whats served is an HTML page with
1. the URL of your Leptos app, which has been compiled to WebAssembly (WASM)
2. the URL of the JavaScript used to initialized this WASM blob
3. an empty `<body>` element
When the JS and WASM have loaded, Leptos will render your app into the `<body>`. This means that nothing appears on the screen until JS/WASM have loaded and run. This has some drawbacks:
1. It increases load time, as your users screen is blank until additional resources have been downloaded.
2. Its bad for SEO, as load times are longer and the HTML you serve has no meaningful content.
3. Its broken for users for whom JS/WASM dont load for some reason (e.g., theyre on a train and just went into a tunnel before WASM finished loading; theyre using an older device that doesnt support WASM; they have JavaScript or WASM turned off for some reason; etc.)
These downsides apply across the web ecosystem, but especially to WASM apps.
So what do you do if you want to return more than just an empty `<body>` tag? Use “server-side rendering.”
Whole books could be (and probably have been) written about this topic, but at its core, its really simple: rather than returning an empty `<body>` tag, return an initial HTML page that reflects the actual starting state of your app or site, so that while JS/WASM are loading, and until they load, the user can access the plain HTML version.
The rest of this section will cover this topic in some detail!

View File

@@ -107,27 +107,28 @@ fn clear() {
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
);
}
```
Well use some manual DOM operations to grab the `<div>` that wraps
the whole component, as well as the `clear` button.
```rust
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
```
Now we can use ordinary DOM APIs to simulate user interaction.
```rust
// now let's click the `clear` button
clear.click();
// now let's click the `clear` button
clear.click();
```
You can test individual DOM element attributes or text node values. Sometimes
@@ -135,27 +136,27 @@ I like to test the whole view at once. We can do this by testing the elements
`outerHTML` against our expectations.
```rust
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
);
```
That test involved us manually replicating the `view` thats inside the component.
@@ -164,15 +165,14 @@ with the initial value `0`. This is where our wrapping element comes in: Ill
the wrappers `innerHTML` against another comparison case.
```rust
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
}
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
```
This is only a very limited introduction to testing. But I hope its useful as you begin to build applications.

View File

@@ -1,14 +1,14 @@
# A Basic Component
That “Hello, world!” was a *very* simple example. Lets move on to something a
That “Hello, world!” was a _very_ simple example. Lets move on to something a
little more like an ordinary app.
First, lets edit the `main` function so that, instead of rendering the whole
app, it just renders an `<App/>` component. Components are the basic unit of
composition and design in most web frameworks, and Leptos is no exception.
Conceptually, they are similar to HTML elements: they represent a section of the
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
`PascalCase`, so most Leptos applications will start with something like an
composition and design in most web frameworks, and Leptos is no exception.
Conceptually, they are similar to HTML elements: they represent a section of the
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
`PascalCase`, so most Leptos applications will start with something like an
`<App/>` component.
```rust
@@ -39,11 +39,12 @@ fn App(cx: Scope) -> impl IntoView {
```
## The Component Signature
```rust
#[component]
```
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
used as a component in your Leptos application. Well see some of the other features of
this macro in a couple chapters.
@@ -52,6 +53,7 @@ fn App(cx: Scope) -> impl IntoView
```
Every component is a function with the following characteristics
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
as its first argument. This `Scope` is our entrypoint into the reactive system.
By convention, its usually named `cx`.
@@ -60,7 +62,8 @@ Every component is a function with the following characteristics
anything you could return from a Leptos `view`.
## The Component Body
The body of the component function is a set-up function that runs once, not a
The body of the component function is a set-up function that runs once, not a
render function that reruns multiple times. Youll typically use it to create a
few reactive variables, define any side effects that run in response to those values
changing, and describe the user interface.
@@ -68,16 +71,17 @@ changing, and describe the user interface.
```rust
let (count, set_count) = create_signal(cx, 0);
```
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
creates a signal, the basic unit of reactive change and state management in Leptos.
This returns a `(getter, setter)` tuple. To access the current value, youll
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
This returns a `(getter, setter)` tuple. To access the current value, youll
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
current value, youll call `set_count.set(...)` (or `set_count(...)`).
> `.get()` clones the value and `.set()` overwrites it. In many cases, its more
efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if youd like to learn more about those trade-offs at this point.
> `.get()` clones the value and `.set()` overwrites it. In many cases, its more
> efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if youd like to learn more about those trade-offs at this point.
## The View
## The View
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
@@ -100,25 +104,28 @@ view! { cx,
This should mostly be easy to understand: it looks like HTML, with a special
`on:click` to define a `click` event listener, a text node thats formatted like
a Rust string, and then...
```rust
{move || count.get()}
```
whatever that is.
People sometimes joke that they use more closures in their first Leptos application
than theyve ever used in their lives. And fair enough. Basically, passing a function
People sometimes joke that they use more closures in their first Leptos application
than theyve ever used in their lives. And fair enough. Basically, passing a function
into the view tells the framework: “Hey, this is something that might change.”
When we click the button and call `set_count`, the `count` signal is updated. This
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
and the framework makes a targeted update to that one specific text node, touching
When we click the button and call `set_count`, the `count` signal is updated. This
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
and the framework makes a targeted update to that one specific text node, touching
nothing else in your application. This is what allows for extremely efficient updates
to the DOM.
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
that this closure is redundant, at least if youre in `nightly` Rust. If youre using
that this closure is redundant, at least if youre in `nightly` Rust. If youre using
Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary.
As a result, you can write a simpler view:
As a result, you can write a simpler view:
```rust
view! { cx,
<button /* ... */>
@@ -129,15 +136,17 @@ view! { cx,
}
```
Remember—and this is *very important*—only functions are reactive. This means that
`{count}` and `{count()}` do very different things in your view. `{count}` passes
Remember—and this is _very important_—only functions are reactive. This means that
`{count}` and `{count()}` do very different things in your view. `{count}` passes
in a function, telling the framework to update the view every time `count` changes.
`{count()}` access the value of `count` once, and passes an `i32` into the view,
`{count()}` access the value of `count` once, and passes an `i32` into the view,
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
> Throughout this tutorial, well use CodeSandbox to show interactive examples. To
show the browser in the sandbox, you may need to click `Add DevTools >
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
and docs for whats going on. Feel free to fork the examples to play with them yourself!
> Throughout this tutorial, well use CodeSandbox to show interactive examples. To
> show the browser in the sandbox, you may need to click `Add DevTools >
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
> and docs for whats going on. Feel free to fork the examples to play with them yourself!
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -1,13 +1,13 @@
# `view`: Dynamic Attributes and Classes
So far weve seen how to use the `view` macro to create event listeners and to
So far weve seen how to use the `view` macro to create event listeners and to
create dynamic text by passing a function (such as a signal) into the view.
But of course there are other things you might want to update in your user interface.
In this section, well look at how to update attributes and classes dynamically,
In this section, well look at how to update attributes and classes dynamically,
and well introduce the concept of a **derived signal**.
Lets start with a simple component that should be familiar: click a button to
Lets start with a simple component that should be familiar: click a button to
increment a counter.
```rust
@@ -20,6 +20,12 @@ fn App(cx: Scope) -> impl IntoView {
on:click=move |_| {
set_count.update(|n| *n += 1);
}
>
"Click me: "
{move || count()}
</button>
}
}
```
So far, this is just the example from the last chapter.
@@ -28,27 +34,31 @@ So far, this is just the example from the last chapter.
Now lets say Id like to update the list of CSS classes on this element dynamically.
For example, lets say I want to add the class `red` when the count is odd. I can
do this using the `class:` syntax.
do this using the `class:` syntax.
```rust
class:red=move || count() & 1 == 1
class:red=move || count() % 2 == 1
```
`class:` attributes take
1. the class name, following the colon (`red`)
2. a value, which can be a `bool` or a function that returns a `bool`
When the value is `true`, the class is added. When the value is `false`, the class
is removed. And if the value is a function that accesses a signal, the class will
is removed. And if the value is a function that accesses a signal, the class will
reactively update when the signal changes.
Now every time I click the button, the text should toggle between red and black as
Now every time I click the button, the text should toggle between red and black as
the number switches between even and odd.
## Dynamic Attributes
The same applies to plain attributes. Passing a plain string or primitive value to
an attribute gives it a static value. Passing a function (including a signal) to
an attribute causes it to update its value reactively. Lets add another element
an attribute causes it to update its value reactively. Lets add another element
to our view:
```rust
<progress
max="50"
@@ -57,17 +67,18 @@ to our view:
/>
```
Now every time we set the count, not only will the `class` of the `<button>` be
toggled, but the `value` of the `<progress>` bar will increase, which means that
Now every time we set the count, not only will the `class` of the `<button>` be
toggled, but the `value` of the `<progress>` bar will increase, which means that
our progress bar will move forward.
## Derived Signals
## Derived Signals
Lets go one layer deeper, just for fun.
You already know that we create reactive interfaces just by passing functions into
You already know that we create reactive interfaces just by passing functions into
the `view`. This means that we can easily change our progress bar. For example,
suppose we want it to move twice as fast:
```rust
<progress
max="50"
@@ -77,28 +88,31 @@ suppose we want it to move twice as fast:
But imagine we want to reuse that calculation in more than one place. You can do this
using a **derived signal**: a closure that accesses a signal.
```rust
let double_count = move || count() * 2;
/* insert the rest of the view */
/* insert the rest of the view */
<progress
max="50"
// we use it once here
value=double_count
/>
<p>
"Double Count: "
"Double Count: "
// and again here
{double_count}
</p>
```
Derived signals let you create reactive computed values that can be used in multiple
Derived signals let you create reactive computed values that can be used in multiple
places in your application with minimal overhead.
> Note: Using a derived signal like this means that the calculation runs once per
signal change per place we access `double_count`; in other words, twice. This is a
very cheap calculation, so thats fine. Well look at memos in a later chapter, which
are designed to solve this problem for expensive calculations.
> Note: Using a derived signal like this means that the calculation runs once per
> signal change per place we access `double_count`; in other words, twice. This is a
> very cheap calculation, so thats fine. Well look at memos in a later chapter, which
> are designed to solve this problem for expensive calculations.
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -1,11 +1,11 @@
# Components and Props
So far, weve been building our whole application in a single component. This
is fine for really tiny examples, but in any real application youll need to
break the user interface out into multiple components, so you can break your
So far, weve been building our whole application in a single component. This
is fine for really tiny examples, but in any real application youll need to
break the user interface out into multiple components, so you can break your
interface down into smaller, reusable, composable chunks.
Lets take our progress bar example. Imagine that you want two progress bars
Lets take our progress bar example. Imagine that you want two progress bars
instead of one: one that advances one tick per click, one that advances two ticks
per click.
@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
view! {
view! {
<progress
max="50"
value=count
@@ -24,10 +24,11 @@ view! {
max="50"
value=double_count
/>
}
```
But of course, this doesnt scale very well. If you want to add a third progress
bar, you need to add this code another time. And if you want to edit anything
bar, you need to add this code another time. And if you want to edit anything
about it, you need to edit it in triplicate.
Instead, lets create a `<ProgressBar/>` component.
@@ -47,15 +48,15 @@ fn ProgressBar(
}
```
Theres just one problem: `progress` is not defined. Where should it come from?
When we were defining everything manually, we just used the local variable names.
Theres just one problem: `progress` is not defined. Where should it come from?
When we were defining everything manually, we just used the local variable names.
Now we need some way to pass an argument into the component.
## Component Props
## Component Props
We do this using component properties, or “props.” If youve used another frontend
framework, this is probably a familiar idea. Basically, properties are to components
as attributes are to HTML elements: they let you pass additional information into
framework, this is probably a familiar idea. Basically, properties are to components
as attributes are to HTML elements: they let you pass additional information into
the component.
In Leptos, you define props by giving additional arguments to the component function.
@@ -69,7 +70,7 @@ fn ProgressBar(
view! { cx,
<progress
max="50"
// now this works
// now this works
value=progress
/>
}
@@ -92,41 +93,45 @@ fn App(cx: Scope) -> impl IntoView {
}
```
Using a component in the view looks a lot like using an HTML element. Youll
notice that you can easily tell the difference between an element and a component
because components always have `PascalCase` names. You pass the `progress` prop
Using a component in the view looks a lot like using an HTML element. Youll
notice that you can easily tell the difference between an element and a component
because components always have `PascalCase` names. You pass the `progress` prop
in as if it were an HTML element attribute. Simple.
> ### Important Note
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
is what allows us to have named props, when Rust does not have named function parameters.
If youre defining a component in one module and importing it into another, make
sure you include this `ComponentProps` type:
> ### Important Note
>
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
> is what allows us to have named props, when Rust does not have named function parameters.
> If youre defining a component in one module and importing it into another, make
> sure you include this `ComponentProps` type:
>
> `use progress_bar::{ProgressBar, ProgressBarProps};`
>
> **Note**: This is still true as of `0.2.5`, but the requirement has been removed on `main`
> and will not apply to later versions.
### Reactive and Static Props
Youll notice that throughout this example, `progress` takes a reactive
Youll notice that throughout this example, `progress` takes a reactive
`ReadSignal<i32>`, and not a plain `i32`. This is **very important**.
Component props have no special meaning attached to them. A component is simply
a function that runs once to set up the user interface. The only way to tell the
interface to respond to changing is to pass it a signal type. So if you have a
component property that will change over time, like our `progress`, it should
Component props have no special meaning attached to them. A component is simply
a function that runs once to set up the user interface. The only way to tell the
interface to respond to changing is to pass it a signal type. So if you have a
component property that will change over time, like our `progress`, it should
be a signal.
### `optional` Props
### `optional` Props
Right now the `max` setting is hard-coded. Lets take that as a prop too. But
lets add a catch: lets make this prop optional by annotating the particular
Right now the `max` setting is hard-coded. Lets take that as a prop too. But
lets add a catch: lets make this prop optional by annotating the particular
argument to the component function with `#[prop(optional)]`.
```rust
#[component]
fn ProgressBar(
cx: Scope,
// mark this prop optional
// mark this prop optional
// you can specify it or not when you use <ProgressBar/>
#[prop(optional)]
max: u16,
@@ -142,7 +147,7 @@ fn ProgressBar(
```
Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max`
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
on an `optional` is its `Default::default()` value, which for a `u16` is going to
be `0`. In the case of a progress bar, a max value of `0` is not very useful.
@@ -187,20 +192,20 @@ fn App(cx: Scope) -> impl IntoView {
"Click me"
</button>
<ProgressBar progress=count/>
// add a second progress bar
// add a second progress bar
<ProgressBar progress=double_count/>
}
}
```
Hm... this wont compile. It should be pretty easy to understand why: weve declared
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
its a closure that returns an `i32`.
There are a couple ways to handle this. One would be to say: “Well, I know that
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
could just take any function?” If youre savvy, you may know that both these
There are a couple ways to handle this. One would be to say: “Well, I know that
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
could just take any function?” If youre savvy, you may know that both these
implement the trait `Fn() -> i32`. So you could use a generic component:
```rust
@@ -210,8 +215,8 @@ fn ProgressBar<F>(
#[prop(default = 100)]
max: u16,
progress: F
) -> impl IntoView
where
) -> impl IntoView
where
F: Fn() -> i32 + 'static,
{
view! { cx,
@@ -223,27 +228,26 @@ where
}
```
This is a perfectly reasonable way to write this component: `progress` now takes
This is a perfectly reasonable way to write this component: `progress` now takes
any value that implements this `Fn()` trait.
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
or as `progress: impl Fn() -> i32 + 'static,`, in part because theyre actually used to generate
a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
> or as `progress: impl Fn() -> i32 + 'static,`, in part because theyre actually used to generate
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
### `into` Props
### `into` Props
Theres one more way we could implement this, and it would be to use `#[prop(into)]`.
Theres one more way we could implement this, and it would be to use `#[prop(into)]`.
This attribute automatically calls `.into()` on the values you pass as props,
which allows you to easily pass props with different values.
In this case, its helpful to know about the
In this case, its helpful to know about the
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
is an enumerated type that represents any kind of readable reactive signal. It can
be useful when defining APIs for components youll want to reuse while passing
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
is an enumerated type that represents any kind of readable reactive signal. It can
be useful when defining APIs for components youll want to reuse while passing
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
reactive value.
```rust
#[component]
fn ProgressBar(
@@ -252,7 +256,7 @@ fn ProgressBar(
max: u16,
#[prop(into)]
progress: Signal<i32>
) -> impl IntoView
) -> impl IntoView
{
view! { cx,
<progress
@@ -281,12 +285,12 @@ fn App(cx: Scope) -> impl IntoView {
## Documenting Components
This is one of the least essential but most important sections of this book.
Its not strictly necessary to document your components and their props. It may
be very important, depending on the size of your team and your app. But its very
This is one of the least essential but most important sections of this book.
Its not strictly necessary to document your components and their props. It may
be very important, depending on the size of your team and your app. But its very
easy, and bears immediate fruit.
To document a component and its props, you can simply add doc comments on the
To document a component and its props, you can simply add doc comments on the
component function, and each one of the props:
```rust
@@ -309,9 +313,11 @@ Thats all you need to do. These behave like ordinary Rust doc comments, excep
that you can document individual component props, which cant be done with Rust
function arguments.
This will automatically generate documentation for your component, its `Props`
type, and each of the fields used to add props. It can be a little hard to
understand how powerful this is until you hover over the component name or props
This will automatically generate documentation for your component, its `Props`
type, and each of the fields used to add props. It can be a little hard to
understand how powerful this is until you hover over the component name or props
and see the power of the `#[component]` macro combined with rust-analyzer here.
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -1,18 +1,19 @@
# Iteration
Whether youre listing todos, displaying a table, or showing product images,
Whether youre listing todos, displaying a table, or showing product images,
iterating over a list of items is a common task in web applications. Reconciling
the differences between changing sets of items can also be one of the trickiest
tasks for a framework to handle well.
Leptos supports to two different patterns for iterating over items:
1. For static views: `Vec<_>`
2. For dynamic lists: `<For/>`
## Static Views with `Vec<_>`
Sometimes you need to show an item repeatedly, but the list youre drawing from
does not often change. In this case, its important to know that you can insert
Sometimes you need to show an item repeatedly, but the list youre drawing from
does not often change. In this case, its important to know that you can insert
any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render
`T`, you can render `Vec<T>`.
@@ -58,31 +59,34 @@ view! { cx,
}
```
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
it changes, this will rerender every item in the list. This is quite inefficient!
Fortunately, theres a better way.
## Dynamic Rendering with the `<For/>` Component
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
keyed dynamic list. It takes three props:
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
- `view`: renders each `T` into a view
- `view`: renders each `T` into a view
`key` is, well, the key. You can add, remove, and move items within the list. As
long as each items key is stable over time, the framework does not need to rerender
any of the items, unless they are new additions, and it can very efficiently add,
remove, and move items as they change. This allows for extremely efficient updates
remove, and move items as they change. This allows for extremely efficient updates
to the list as it changes, with minimal additional work.
Creating a good `key` can be a little tricky. You generally do _not_ want to use
an index for this purpose, as it is not stable—if you remove or move items, their
Creating a good `key` can be a little tricky. You generally do _not_ want to use
an index for this purpose, as it is not stable—if you remove or move items, their
indices change.
But its a great idea to do something like generating a unique ID for each row as
But its a great idea to do something like generating a unique ID for each row as
it is generated, and using that as an ID for the key function.
Check out the `<DynamicList/>` component below for an example.
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -1,23 +1,24 @@
# Forms and Inputs
Forms and form inputs are an important part of interactive apps. There are two
Forms and form inputs are an important part of interactive apps. There are two
basic patterns for interacting with inputs in Leptos, which you may recognize
if youre familiar with React, SolidJS, or a similar framework: using **controlled**
or **uncontrolled** inputs.
## Controlled Inputs
In a "controlled input," the framework controls the state of the input
element. On every `input` event, it updates a local signal that holds the current
In a "controlled input," the framework controls the state of the input
element. On every `input` event, it updates a local signal that holds the current
state, which in turn updates the `value` prop of the input.
There are two important things to remember:
1. The `input` event fires on (almost) every change to the element, while the
`change` event fires (more or less) when you unfocus the input. You probably
1. The `input` event fires on (almost) every change to the element, while the
`change` event fires (more or less) when you unfocus the input. You probably
want `on:input`, but we give you the freedom to choose.
2. The `value` *attribute* only sets the initial value of the input, i.e., it
only updates the input up to the point that you begin typing. The `value`
*property* continues updating the input after that. You usually want to set
2. The `value` _attribute_ only sets the initial value of the input, i.e., it
only updates the input up to the point that you begin typing. The `value`
_property_ continues updating the input after that. You usually want to set
`prop:value` for this reason.
```rust
@@ -41,14 +42,14 @@ view! { cx,
}
```
## Uncontrolled Inputs
## Uncontrolled Inputs
In an "uncontrolled input," the browser controls the state of the input element.
Rather than continuously updating a signal to hold its value, we use a
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
In an "uncontrolled input," the browser controls the state of the input element.
Rather than continuously updating a signal to hold its value, we use a
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
the input once when we want to get its value.
In this example, we only notify the framework when the `<form>` fires a `submit`
In this example, we only notify the framework when the `<form>` fires a `submit`
event.
```rust
@@ -56,7 +57,8 @@ let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
let input_element: NodeRef<Input> = create_node_ref(cx);
```
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
underlying DOM node. Its value will be set when the element is rendered.
```rust
@@ -76,13 +78,14 @@ let on_submit = move |ev: SubmitEvent| {
set_name(value);
};
```
Our `on_submit` handler will access the inputs value and use it to call `set_name`.
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
know it will already have been filled when we rendered the view, so its safe to
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
know it will already have been filled when we rendered the view, so its safe to
unwrap here.
We can then call `.value()` to get the value out of the input, because `NodeRef`
We can then call `.value()` to get the value out of the input, because `NodeRef`
gives us access to a correctly-typed HTML element.
```rust
@@ -97,11 +100,15 @@ view! { cx,
<p>"Name is: " {name}</p>
}
```
The view should be pretty self-explanatory by now. Note two things:
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
This is because were just setting the initial value of the input, and letting
This is because were just setting the initial value of the input, and letting
the browser control its state. (We could use `prop:value` instead.)
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
They are the same thing, but `node_ref` has better rust-analyzer support.)
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -282,4 +282,6 @@ view! { cx,
}
```
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -110,4 +110,6 @@ Not a number! Errors:
If you fix the error, the error message will disappear and the content youre wrapping in
an `<ErrorBoundary/>` will appear again.
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -15,7 +15,7 @@ covered some of this in the material on [components and props](./03_components.m
Basically if you want the parent to communicate to the child, you can pass a
[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/struct.MaybeSignal.html) as a prop.
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) as a prop.
But what about the other direction? How can a child send notifications about events
or state changes back up to the parent?
@@ -285,4 +285,6 @@ in `<ButtonD/>` and a single text node in `<App/>`. Its as if the components
themselves dont exist at all. And, well... at runtime, they dont. Its just
signals and effects, all the way down.
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -123,4 +123,6 @@ view! { cx,
}
```
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

BIN
docs/video/async.mov Normal file

Binary file not shown.

BIN
docs/video/in-order.mov Normal file

Binary file not shown.

BIN
docs/video/out-of-order.mov Normal file

Binary file not shown.

60
examples/Makefile.toml Normal file
View File

@@ -0,0 +1,60 @@
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
# Emulate workspace
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
#"counters", - FIXME: test compile errors
"counters_stable",
"counter_without_macros",
"error_boundary",
"errors_axum",
"fetch",
"hackernews",
"hackernews_axum",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"ssr_modes",
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
[tasks.verify]
description = "Run all quality checks and tests"
dependencies = ["check-style", "test-unit-and-web"]
[tasks.test-unit-and-web]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.pre-verify-flow]
[tasks.post-verify-flow]
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
[tasks.pre-web-test-flow]
[tasks.web-test]
[tasks.post-web-test-flow]

View File

@@ -3,14 +3,17 @@ name = "counter"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys ="0.3"
web-sys = "0.3"

View File

@@ -1,4 +1,4 @@
use counter::*;
use counter::SimpleCounter;
use leptos::*;
pub fn main() {

View File

@@ -6,11 +6,15 @@ edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
broadcaster = "1"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"

View File

@@ -198,13 +198,13 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
let s = create_signal_from_stream(
cx,
source.subscribe("message").unwrap().map(|value| {
value
.expect("no message event")
.1
.data()
.as_string()
.expect("expected string value")
}),
match value {
Ok(value) => {
value.1.data().as_string().expect("expected string value")
},
Err(_) => "0".to_string(),
}
})
);
on_cleanup(cx, move || source.close());

View File

@@ -3,11 +3,21 @@ name = "counter_without_macros"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.34"
pretty_assertions = "1.3.0"
[dev-dependencies.web-sys]
features = ["HtmlElement", "XPathResult"]
version = "0.3.61"

View File

@@ -1,3 +1,10 @@
[env]
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
[tasks.web-test]
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.build]
command = "cargo"
args = ["+stable", "build-all-features"]

View File

@@ -3,3 +3,5 @@
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
Issue the `cargo make test-flow` command to run unit and wasm tests.

View File

@@ -1,48 +1,44 @@
use leptos::{ev, html::*, *};
pub struct Props {
/// The starting value for the counter
pub initial_value: i32,
/// The change that should be applied each time the button is clicked.
pub step: i32,
}
/// A simple counter view.
pub fn view(cx: Scope, props: Props) -> impl IntoView {
let Props {
initial_value,
step,
} = props;
// 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: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
div(cx)
.child((
cx,
// children can be added with .child()
// this takes any type that implements IntoView as its argument
// for example, a string or an HtmlElement<_>
.child(
button(cx)
// 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_value.update(|value| *value = 0))
.child((cx, "Clear")),
))
.child((
cx,
.child("Clear"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value -= step)
})
.child((cx, "-1")),
))
.child((
cx,
.child("-1"),
)
.child(
span(cx)
.child((cx, "Value: "))
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child((cx, move || value.get()))
.child((cx, "!")),
))
.child((
cx,
.child("!"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value += step)
})
.child((cx, "+1")),
))
.child("+1"),
)
}

View File

@@ -1,16 +1,8 @@
use counter_without_macros as counter;
use counter_without_macros::counter;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
counter::view(
cx,
counter::Props {
initial_value: 0,
step: 1,
},
)
})
mount_to_body(|cx| counter(cx, 0, 1))
}

View File

@@ -1,58 +0,0 @@
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counter_without_macros as counter;
use leptos::*;
use web_sys::HtmlElement;
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| {
counter::view(
cx,
counter::Props {
initial_value: 0,
step: 1,
},
)
});
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let clear = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
let dec = clear
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
let text = dec
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
let inc = text
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
inc.click();
inc.click();
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
dec.click();
dec.click();
dec.click();
dec.click();
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
clear.click();
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
}

View File

@@ -0,0 +1,86 @@
use counter_without_macros::counter;
use leptos::*;
use pretty_assertions::assert_eq;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::HtmlElement;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn should_increment_counter() {
open_counter();
click_increment();
click_increment();
assert_eq!(see_text(), Some("Value: 2!".to_string()));
}
#[wasm_bindgen_test]
fn should_decrement_counter() {
open_counter();
click_decrement();
click_decrement();
assert_eq!(see_text(), Some("Value: -2!".to_string()));
}
#[wasm_bindgen_test]
fn should_clear_counter() {
open_counter();
click_increment();
click_increment();
click_clear();
assert_eq!(see_text(), Some("Value: 0!".to_string()));
}
fn open_counter() {
remove_existing_counter();
mount_to_body(move |cx| counter(cx, 0, 1));
}
fn remove_existing_counter() {
if let Some(counter) =
leptos::document().query_selector("body div").unwrap()
{
counter.remove();
}
}
fn click_clear() {
click_text("Clear");
}
fn click_decrement() {
click_text("-1");
}
fn click_increment() {
click_text("+1");
}
fn click_text(text: &str) {
find_by_text(text).click();
}
fn see_text() -> Option<String> {
find_by_text("Value: ").text_content()
}
fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = leptos::document();
document
.evaluate(&xpath, &document)
.unwrap()
.iterate_next()
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
}

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

@@ -1,4 +1,4 @@
use counters::{Counters, CountersProps};
use counters::Counters;
use leptos::*;
fn main() {

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

@@ -3,8 +3,12 @@ name = "error_boundary"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
@@ -28,8 +28,6 @@ thiserror = "1.0.38"
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
@@ -44,7 +42,7 @@ ssr = [
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name

View File

@@ -70,8 +70,8 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
</p>
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
<div>
// note that the error boundries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribue to the
// note that the error boundaries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribute to the
// final status code sent by the server when producing ssr pages.
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ReturnsError/>

View File

@@ -3,15 +3,18 @@ name = "fetch"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
anyhow = "1.0.58"
leptos = { path = "../../leptos" }
reqwasm = "0.5.0"
reqwasm = "0.5"
serde = { version = "1", features = ["derive"] }
log = "0.4"
console_log = "0.2"
console_error_panic_hook = "0.1.7"
console_log = "1"
console_error_panic_hook = "0.1"
thiserror = "1"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen-test = "0.3"

View File

@@ -1,38 +1,50 @@
use anyhow::Result;
use leptos::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cat {
url: String,
}
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
#[derive(Error, Clone, Debug)]
pub enum FetchError {
#[error("Please request more than zero cats.")]
NonZeroCats,
#[error("Error loading data from serving.")]
Request,
#[error("Error deserializaing cat data from request.")]
Json
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
if count > 0 {
// make the request
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={count}",
))
.send()
.await?
.await
.map_err(|_| FetchError::Request)?
// convert it to JSON
.json::<Vec<Cat>>()
.await?
.await
.map_err(|_| FetchError::Json)?
// extract the URL field for each cat
.into_iter()
.map(|cat| cat.url)
.collect::<Vec<_>>();
Ok(res)
} else {
Ok(vec![])
Err(FetchError::NonZeroCats)
}
}
pub fn fetch_example(cx: Scope) -> impl IntoView {
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 0);
// we use local_resource here because
// 1) anyhow::Result isn't serializable/deserializable
// 1) our error type isn't serializable/deserializable
// 2) we're not doing server-side rendering in this example anyway
// (during SSR, create_resource will begin loading on the server and resolve on the client)
let cats = create_local_resource(cx, cat_count, fetch_cats);
@@ -42,7 +54,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
errors.with(|errors| {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
.collect::<Vec<_>>()
})
};
@@ -60,11 +72,12 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just implement our happy path and let the framework handle the rest
let cats_view = move || {
cats.with(cx, |data| {
data.iter()
.flatten()
.map(|cat| view! { cx, <img src={cat}/> })
.collect::<Vec<_>>()
cats.read(cx).map(|data| {
data.map(|data| {
data.iter()
.map(|s| view! { cx, <span>{s}</span> })
.collect::<Vec<_>>()
})
})
};
@@ -72,8 +85,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
<div>
<label>
"How many cats would you like?"
<input type="number"
prop:value={move || cat_count.get().to_string()}
<input
type="number"
prop:value=move || cat_count.get().to_string()
on:input=move |ev| {
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
set_cat_count(val);
@@ -81,7 +95,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
/>
</label>
<ErrorBoundary fallback>
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
<Transition fallback=move || {
view! { cx, <div>"Loading (Suspense Fallback)..."</div> }
}>
{cats_view}
</Transition>
</ErrorBoundary>

View File

@@ -6,10 +6,14 @@ edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -38,7 +38,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link =
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
move |cx| pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@@ -65,16 +65,20 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
<span class="page-link"
class:disabled=move || hide_more_link(cx)
aria-hidden=move || hide_more_link(cx)
>
"more >"
</a>
</span>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
</div>
<main class="news-list">
<div>

View File

@@ -6,8 +6,12 @@ edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -1,6 +1,10 @@
[workspace]
members = ["client", "api-boundary", "server"]
[profile.release]
codegen-units = 1
lto = true
[patch.crates-io]
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router" }

View File

@@ -12,7 +12,7 @@ leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
log = "0.4"
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
gloo-net = "0.2"
gloo-storage = "0.2"
serde = "1.0"

View File

@@ -1,3 +1,3 @@
[[proxy]]
rewrite = "/api/"
backend = "http://0.0.0.0:3000/"
backend = "http://127.0.0.1:3000/"

View File

@@ -3,9 +3,13 @@ name = "parent-child"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
web-sys = "0.3"

View File

@@ -3,8 +3,12 @@ name = "router"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "0.2"
console_log = "1"
log = "0.4"
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router", features = ["csr"] }

View File

@@ -3,17 +3,7 @@
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<style>
a[aria-current] {
font-weight: bold;
}
.contact, .contact-list {
border: 1px solid #c0c0c0;
border-radius: 3px;
padding: 1rem;
}
</style>
<link data-trunk rel="css" href="style.css"/>
</head>
<body></body>
</html>
</html>

View File

@@ -27,20 +27,13 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
<A href="redirect-home">"Redirect to Home"</A>
</nav>
<main>
<Routes>
<Route
path=""
view=move |cx| view! { cx, <ContactList/> }
>
<Route
path=":id"
view=move |cx| view! { cx, <Contact/> }
/>
<Route
path="/"
view=move |_| view! { cx, <p>"Select a contact."</p> }
/>
</Route>
<AnimatedRoutes
outro="slideOut"
intro="slideIn"
outro_back="slideOutBack"
intro_back="slideInBack"
>
<ContactRoutes/>
<Route
path="about"
view=move |cx| view! { cx, <About/> }
@@ -53,12 +46,33 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
path="redirect-home"
view=move |cx| view! { cx, <Redirect path="/"/> }
/>
</Routes>
</AnimatedRoutes>
</main>
</Router>
}
}
// You can define other routes in their own component.
// Use a #[component(transparent)] that returns a <Route/>.
#[component(transparent)]
pub fn ContactRoutes(cx: Scope) -> impl IntoView {
view! { cx,
<Route
path=""
view=move |cx| view! { cx, <ContactList/> }
>
<Route
path=":id"
view=move |cx| view! { cx, <Contact/> }
/>
<Route
path="/"
view=move |_| view! { cx, <p>"Select a contact."</p> }
/>
</Route>
}
}
#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
log::debug!("rendering <ContactList/>");
@@ -93,7 +107,11 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
{move || view! { cx, <ul>{contacts}</ul>}}
</Suspense>
<Outlet/>
<AnimatedOutlet
class="outlet"
outro="fadeOut"
intro="fadeIn"
/>
</div>
}
}

95
examples/router/style.css Normal file
View File

@@ -0,0 +1,95 @@
a[aria-current] {
font-weight: bold;
}
.outlet {
border: 1px dotted grey;
}
.contact, .contact-list {
border: 1px solid #c0c0c0;
border-radius: 3px;
padding: 1rem;
}
.fadeIn {
animation: 0.5s fadeIn forwards;
}
.fadeOut {
animation: 0.5s fadeOut forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.slideIn {
animation: 0.125s slideIn forwards;
}
.slideOut {
animation: 0.125s slideOut forwards;
}
@keyframes slideIn {
from {
transform: translate(100vw, 0);
}
to {
transform: translate(0px, 0px);
}
}
@keyframes slideOut {
from {
transform: translate(0px, 0px);
}
to {
transform: translate(-100vw, 0);
}
}
.slideInBack {
animation: 0.125s slideInBack forwards;
}
.slideOutBack {
animation: 0.125s slideOutBack forwards;
}
@keyframes slideInBack {
from {
transform: translate(-100vw, 0);
}
to {
transform: translate(0px, 0px);
}
}
@keyframes slideOutBack {
from {
transform: translate(0px, 0px);
}
to {
transform: translate(100vw, 0);
}
}

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
console_log = "1.0.0"
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
@@ -43,8 +43,6 @@ bcrypt = { version = "0.14", optional = true }
async-trait = { version = "0.1.64", optional = true }
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
@@ -65,7 +63,7 @@ ssr = [
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name

View File

@@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@@ -16,12 +19,15 @@
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@@ -61,11 +67,11 @@
]
},
"locked": {
"lastModified": 1677292251,
"narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=",
"lastModified": 1681525152,
"narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc",
"rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a",
"type": "github"
},
"original": {
@@ -73,6 +79,36 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -6,7 +6,7 @@ if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::{post, get},
extract::{Path, Extension},
extract::{Path, Extension, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
Router,
@@ -22,11 +22,12 @@ if #[cfg(feature = "ssr")] {
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
use axum_sessions_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, request: Request<AxumBody>) -> impl IntoResponse {
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
request: Request<AxumBody>) -> impl IntoResponse {
log!("{:?}", path);
handle_server_fns_with_context(path, headers, move |cx| {
handle_server_fns_with_context(path, headers, raw_query, move |cx| {
provide_context(cx, auth_session.clone());
provide_context(cx, pool.clone());
}, request).await
@@ -73,7 +74,7 @@ if #[cfg(feature = "ssr")] {
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(server_fn_handler))
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
.fallback(file_and_error_handler)
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [
@@ -36,6 +36,10 @@ ssr = [
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [
@@ -39,6 +39,10 @@ ssr = [
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"

View File

@@ -22,7 +22,7 @@ cfg-if = "1.0"
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2", optional = true }
console_log = { version = "0.2", optional = true }
console_log = { version = "1", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# dependecies for server (enable when ssr set)

View File

@@ -16,6 +16,6 @@ gloo-net = { version = "0.2", features = ["http"] }
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2" }
console_log = { version = "0.2"}
console_log = { version = "1"}
console_error_panic_hook = { version = "0.1"}

View File

@@ -11,7 +11,7 @@ actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
anyhow = "1.0.68"
broadcaster = "1.0.0"
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
serde = { version = "1.0.152", features = ["derive"] }
futures = "0.3.25"

View File

@@ -1,19 +1,23 @@
# Leptos Todo App Sqlite
# Leptos Todo App Sqlite
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
## Client Side Rendering
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. Make sure you have trunk installed with `cargo install trunk`.
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
## 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
```
@@ -21,24 +25,30 @@ 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 `"."`. You'll 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
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"

View File

@@ -18,6 +18,7 @@ cfg_if! {
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
_ = FormDataHandler::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@@ -106,6 +107,24 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct FormData {
hi: String
}
#[server(FormDataHandler, "/api")]
pub async fn form_data(cx: Scope) -> Result<FormData, ServerFnError> {
use axum::extract::FromRequest;
let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx).and_then(|req| req.take_request()).unwrap();
if req.method() == http::Method::POST {
let form = axum::Form::from_request(req, &()).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(form.0)
} else {
Err(ServerFnError::ServerError("wrong form fields submitted".to_string()))
}
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
//let id = use_context::<String>(cx);
@@ -126,6 +145,23 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
<Todos/>
</ErrorBoundary>
}/> //Route
<Route path="weird" methods=&[Method::Get, Method::Post]
ssr=SsrMode::Async
view=|cx| {
let res = create_resource(cx, || (), move |_| async move {
form_data(cx).await
});
view! { cx,
<Suspense fallback=|| ()>
<pre>
{move || {
res.with(cx, |body| format!("{body:#?}"))
}}
</pre>
</Suspense>
}
}
/>
</Routes>
</main>
</Router>
@@ -147,6 +183,10 @@ pub fn Todos(cx: Scope) -> impl IntoView {
view! {
cx,
<form method="POST" action="/weird">
<input type="text" name="hi" value="John"/>
<input type="submit"/>
</form>
<div>
<MultiActionForm action=add_todo>
<label>

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"

View File

@@ -51,7 +51,8 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await

View File

@@ -3,10 +3,14 @@ name = "todomvc"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", default-features = false }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }
@@ -21,3 +25,6 @@ default = ["csr"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
[package.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -14,5 +14,6 @@ leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
serde_json = "1"
parking_lot = "0.12.1"
regex = "1.7.0"

View File

@@ -18,9 +18,10 @@ use http::StatusCode;
use leptos::{
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
*,
};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
@@ -94,7 +95,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -150,9 +151,9 @@ pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|_cx| {})
}
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
/// Leptos server function arguments in the body, runs the server function if found,
/// and returns the resulting [HttpResponse].
/// An Actix [Route](actix_web::Route) that listens for `GET` or `POST` requests with
/// Leptos server function arguments in the URL (`GET`) or body (`POST`),
/// runs the server function if found, and returns the resulting [HttpResponse].
///
/// This provides the [HttpRequest] to the server [Scope](leptos::Scope).
///
@@ -168,7 +169,7 @@ pub fn handle_server_fns() -> Route {
pub fn handle_server_fns_with_context(
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> Route {
web::post().to(
web::to(
move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| {
let additional_context = additional_context.clone();
async move {
@@ -194,15 +195,17 @@ pub fn handle_server_fns_with_context(
provide_context(cx, req.clone());
provide_context(cx, res_options.clone());
match server_fn(cx, body).await {
let query = req.query_string().as_bytes();
let data = match &server_fn.encoding {
Encoding::Url | Encoding::Cbor => body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let res = match (server_fn.trait_obj)(cx, data).await {
Ok(serialized) => {
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
let mut res: HttpResponseBuilder;
let mut res_parts = res_options.0.write();
@@ -257,9 +260,15 @@ pub fn handle_server_fns_with_context(
}
}
}
Err(e) => HttpResponse::InternalServerError()
.body(e.to_string()),
}
Err(e) => HttpResponse::InternalServerError().body(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
),
};
// clean up the scope
disposer.dispose();
runtime.dispose();
res
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
@@ -289,6 +298,7 @@ pub fn handle_server_fns_with_context(
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -312,6 +322,7 @@ pub fn handle_server_fns_with_context(
/// leptos_actix::render_app_to_stream(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// Method::Get,
/// ),
/// )
/// })
@@ -331,28 +342,31 @@ pub fn handle_server_fns_with_context(
pub fn render_app_to_stream<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_with_context(options, |_cx| {}, app_fn)
render_app_to_stream_with_context(options, |_cx| {}, app_fn, method)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
/// the documentation for that function.
/// The HTML stream is rendered using
/// [render_to_stream_in_order](leptos::ssr::render_to_stream_in_order),
/// and includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -376,6 +390,7 @@ where
/// leptos_actix::render_app_to_stream_in_order(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// Method::Get,
/// ),
/// )
/// })
@@ -395,11 +410,17 @@ where
pub fn render_app_to_stream_in_order<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_in_order_with_context(options, |_cx| {}, app_fn)
render_app_to_stream_in_order_with_context(
options,
|_cx| {},
app_fn,
method,
)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -409,13 +430,14 @@ where
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
/// the documentation for that function.
/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::render_to_string_async), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -439,6 +461,7 @@ where
/// leptos_actix::render_app_async(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// Method::Get,
/// ),
/// )
/// })
@@ -458,11 +481,12 @@ where
pub fn render_app_async<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_async_with_context(options, |_cx| {}, app_fn)
render_app_async_with_context(options, |_cx| {}, app_fn, method)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -481,11 +505,12 @@ pub fn render_app_to_stream_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
@@ -503,7 +528,14 @@ where
stream_app(&options, app, res_options, additional_context).await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -522,11 +554,12 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
@@ -545,7 +578,14 @@ where
stream_app_in_order(&options, app, res_options, additional_context)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -565,11 +605,12 @@ pub fn render_app_async_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
@@ -593,7 +634,14 @@ where
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -728,7 +776,7 @@ async fn stream_app(
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| generate_head_metadata(cx).into(),
move |cx| generate_head_metadata_separated(cx).1.into(),
additional_context,
);
@@ -745,7 +793,7 @@ async fn stream_app_in_order(
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move |cx| {
generate_head_metadata(cx).into()
generate_head_metadata_separated(cx).1.into()
},
additional_context,
);
@@ -762,7 +810,7 @@ async fn build_stream_response(
) -> HttpResponse {
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
@@ -844,7 +892,7 @@ async fn render_app_async_helper(
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
) -> Vec<(String, SsrMode)>
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -853,11 +901,16 @@ where
// Empty strings screw with Actix pathing, they need to be "/"
routes = routes
.into_iter()
.map(|(s, mode)| {
if s.is_empty() {
return ("/".to_string(), mode);
.map(|listing| {
let path = listing.path();
if path.is_empty() {
return RouteListing::new(
"/".to_string(),
listing.mode(),
listing.methods(),
);
}
(s, mode)
RouteListing::new(listing.path(), listing.mode(), listing.methods())
})
.collect();
@@ -867,14 +920,19 @@ where
// Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}`
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
let routes: Vec<(String, SsrMode)> = routes
let routes = routes
.into_iter()
.map(|(s, m)| (wildcard_re.replace_all(&s, "{tail:.*}").to_string(), m))
.map(|(s, m)| (capture_re.replace_all(&s, "{$1}").to_string(), m))
.collect();
.map(|listing| {
let path = wildcard_re
.replace_all(listing.path(), "{tail:.*}")
.to_string();
let path = capture_re.replace_all(&path, "{$1}").to_string();
RouteListing::new(path, listing.mode(), listing.methods())
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![("/".to_string(), Default::default())]
vec![RouteListing::new("/", Default::default(), [Method::Get])]
} else {
routes
}
@@ -891,7 +949,7 @@ pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -916,7 +974,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -938,7 +996,7 @@ where
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -978,7 +1036,7 @@ where
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -986,29 +1044,39 @@ where
IV: IntoView + 'static,
{
let mut router = self;
for (path, mode) in paths.iter() {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
for listing in paths.iter() {
let path = listing.path();
let mode = listing.mode();
for method in listing.methods() {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)
}
SsrMode::Async => render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
},
);
method,
),
},
);
}
}
router
}

View File

@@ -16,5 +16,8 @@ leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"
tokio-util = {version = "0.7.7", features = ["rt"] }
once_cell = "1.17"

View File

@@ -8,13 +8,13 @@
use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::Path,
extract::{Path, RawQuery},
http::{
header::{HeaderName, HeaderValue},
HeaderMap, Request, StatusCode,
},
response::IntoResponse,
routing::get,
routing::{delete, get, patch, post, put},
};
use futures::{
channel::mpsc::{Receiver, Sender},
@@ -24,15 +24,18 @@ use http::{header, method::Method, uri::Uri, version::Version, Response};
use hyper::body;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
ssr::*,
*,
};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::{generate_head_metadata, MetaContext};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_router::*;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc};
use tokio::task::{spawn_blocking, LocalSet};
use std::{io, pin::Pin, sync::Arc, thread::available_parallelism};
use tokio::task::LocalSet;
use tokio_util::task::LocalPoolHandle;
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
/// to construct this for Leptos to use in Axum
@@ -95,7 +98,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -128,7 +131,7 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
/// Decomposes an HTTP request into its parts, allowing you to read its headers
/// and other data without consuming the body. Creates a new Request from the
/// original parts for further processsing
/// original parts for further processing
pub async fn generate_request_and_parts(
req: Request<Body>,
) -> (Request<Body>, RequestParts) {
@@ -147,8 +150,9 @@ pub async fn generate_request_and_parts(
(request, request_parts)
}
/// A struct to hold the http::request::Request and allow users to take ownership of it
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
/// A struct to hold the [`http::request::Request`] and allow users to take ownership of it
/// Required by `Request` not being `Clone`. See
/// [this issue](https://github.com/hyperium/http/pull/574) for eventual resolution:
#[derive(Debug, Default)]
pub struct LeptosRequest<B>(Arc<RwLock<Option<Request<B>>>>);
@@ -158,12 +162,12 @@ impl<B> Clone for LeptosRequest<B> {
}
}
impl<B> LeptosRequest<B> {
/// Overwrite the contents of a LeptosRequest with a new Request<B>
/// Overwrite the contents of a LeptosRequest with a new `Request<B>`
pub fn overwrite(&self, req: Option<Request<B>>) {
let mut writable = self.0.write();
*writable = req
}
/// Consume the inner Request<B> inside the LeptosRequest and return it
/// Consume the inner `Request<B>` inside the LeptosRequest and return it
///```rust, ignore
/// use axum::{
/// RequestPartsExt,
@@ -198,8 +202,9 @@ impl<B> LeptosRequest<B> {
}
}
/// Generate a wrapper for the http::Request::Request type that allows one to
/// processs it, access the body, and use axum Extractors on it.
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
/// process it, access the body, and use axum Extractors on it.
/// Required by Request not being Clone. See
/// [this issue](https://github.com/hyperium/http/pull/574) for eventual resolution:
pub async fn generate_leptos_request<B>(req: Request<B>) -> LeptosRequest<B>
where
B: Default + std::fmt::Debug,
@@ -246,9 +251,10 @@ where
pub async fn handle_server_fns(
Path(fn_name): Path<String>,
headers: HeaderMap,
RawQuery(query): RawQuery,
req: Request<Body>,
) -> impl IntoResponse {
handle_server_fns_inner(fn_name, headers, |_| {}, req).await
handle_server_fns_inner(fn_name, headers, query, |_| {}, req).await
}
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
@@ -268,15 +274,18 @@ pub async fn handle_server_fns(
pub async fn handle_server_fns_with_context(
Path(fn_name): Path<String>,
headers: HeaderMap,
RawQuery(query): RawQuery,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
handle_server_fns_inner(fn_name, headers, additional_context, req).await
handle_server_fns_inner(fn_name, headers, query, additional_context, req)
.await
}
async fn handle_server_fns_inner(
fn_name: String,
headers: HeaderMap,
query: Option<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
@@ -287,133 +296,116 @@ async fn handle_server_fns_inner(
.unwrap_or(fn_name);
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
async move {
let res = if let Some(server_fn) =
server_fn_by_path(fn_name.as_str())
let pool_handle = get_leptos_pool();
pool_handle.spawn_pinned(move || {
async move {
let res = if let Some(server_fn) =
server_fn_by_path(fn_name.as_str())
{
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
additional_context(cx);
let (req, req_parts) = generate_request_and_parts(req).await;
let leptos_req = generate_leptos_request(req).await; // Add this so we can get details about the Request
provide_context(cx, req_parts.clone());
provide_context(cx, leptos_req);
// Add this so that we can set headers and status of the response
provide_context(cx, ResponseOptions::default());
let query: &Bytes = &query.unwrap_or("".to_string()).into();
let data = match &server_fn.encoding {
Encoding::Url | Encoding::Cbor => &req_parts.body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let res = match (server_fn.trait_obj)(cx, data).await {
Ok(serialized) => {
// If ResponseOptions are set, add the headers and status to the request
let res_options = use_context::<ResponseOptions>(cx);
// if this is Accept: application/json then send a serialized JSON response
let accept_header = headers
.get("Accept")
.and_then(|value| value.to_str().ok());
let mut res = Response::builder();
// Add headers from ResponseParts if they exist. These should be added as long
// as the server function returns an OK response
let res_options_outer = res_options.unwrap().0;
let res_options_inner = res_options_outer.read();
let (status, mut res_headers) = (
res_options_inner.status,
res_options_inner.headers.clone(),
);
if accept_header == Some("application/json")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
let runtime = create_runtime();
let (cx, disposer) =
raw_scope_and_disposer(runtime);
additional_context(cx);
let (req, req_parts) =
generate_request_and_parts(req).await;
let leptos_req = generate_leptos_request(req).await; // Add this so we can get details about the Request
provide_context(cx, req_parts.clone());
provide_context(cx, leptos_req);
// Add this so that we can set headers and status of the response
provide_context(cx, ResponseOptions::default());
match server_fn(cx, &req_parts.body).await {
Ok(serialized) => {
// If ResponseOptions are set, add the headers and status to the request
let res_options =
use_context::<ResponseOptions>(cx);
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
let accept_header = headers
.get("Accept")
.and_then(|value| value.to_str().ok());
let mut res = Response::builder();
// Add headers from ResponseParts if they exist. These should be added as long
// as the server function returns an OK response
let res_options_outer =
res_options.unwrap().0;
let res_options_inner =
res_options_outer.read();
let (status, mut res_headers) = (
res_options_inner.status,
res_options_inner.headers.clone(),
);
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header
== Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
let referer = headers
.get("Referer")
.and_then(|value| {
value.to_str().ok()
})
.unwrap_or("/");
res = res
.status(StatusCode::SEE_OTHER)
.header("Location", referer);
}
// Override StatusCode if it was set in a Resource or Element
res = match status {
Some(status) => res.status(status),
None => res,
};
// This must be after the default referrer
// redirect so that it overwrites the one above
if let Some(header_ref) = res.headers_mut()
{
header_ref.extend(res_headers.drain());
};
match serialized {
Payload::Binary(data) => res
.header(
"Content-Type",
"application/cbor",
)
.body(Full::from(data)),
Payload::Url(data) => res
.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
)
.body(Full::from(data)),
Payload::Json(data) => res
.header(
"Content-Type",
"application/json",
)
.body(Full::from(data)),
}
}
Err(e) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::from(e.to_string())),
}
} else {
Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::from(format!(
"Could not find a server function at the \
route {fn_name}. \n\nIt's likely that \
you need to call ServerFn::register() on \
the server function type, somewhere in \
your `main` function."
)))
res = res.status(StatusCode::OK);
}
.expect("could not build Response");
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
let referer = headers
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
_ = tx.send(res);
res = res
.status(StatusCode::SEE_OTHER)
.header("Location", referer);
}
// Override StatusCode if it was set in a Resource or Element
res = match status {
Some(status) => res.status(status),
None => res,
};
// This must be after the default referrer
// redirect so that it overwrites the one above
if let Some(header_ref) = res.headers_mut() {
header_ref.extend(res_headers.drain());
};
match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(Full::from(data)),
Payload::Url(data) => res
.header(
"Content-Type",
"application/x-www-form-urlencoded",
)
.body(Full::from(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(Full::from(data)),
}
}
})
Err(e) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::from(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
)),
};
// clean up the scope
disposer.dispose();
runtime.dispose();
res
} else {
Response::builder().status(StatusCode::BAD_REQUEST).body(
Full::from(format!(
"Could not find a server function at the route \
{fn_name}. \n\nIt's likely that you need to call \
ServerFn::register() on the server function type, \
somewhere in your `main` function."
)),
)
}
.expect("could not build Response");
_ = tx.send(res);
}
});
@@ -495,7 +487,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
@@ -617,56 +609,33 @@ where
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
let local_pool = get_leptos_pool();
let (tx, rx) = futures::channel::mpsc::channel(8);
local_pool.spawn_pinned(move || async move {
let app = {
// Need to get the path and query string of the Request
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();
async move {
// Need to get the path and query string of the Request
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
let path = req.uri().path_and_query().unwrap().as_str();
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::mpsc::channel(8);
spawn_blocking({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
async move {
tokio::task::LocalSet::new()
.run_until(async {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
let leptos_req = generate_leptos_request(req).await;
move |cx| {
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
app_fn(cx).into_view(cx)
}
};
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata(cx).into(),
add_context,
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
})
.await;
}
});
let full_path = format!("http://leptos.dev{path}");
let (req, req_parts) = generate_request_and_parts(req).await;
let leptos_req = generate_leptos_request(req).await;
move |cx| {
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
app_fn(cx).into_view(cx)
}
});
};
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
add_context,
);
generate_response(res_options3, rx).await
}
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
});
async move { generate_response(res_options3, rx).await }
})
}
}
@@ -711,7 +680,7 @@ async fn forward_stream(
) {
let cx = Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
@@ -735,7 +704,7 @@ async fn forward_stream(
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
@@ -796,42 +765,26 @@ where
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::mpsc::channel(8);
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || async move {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
let leptos_req = generate_leptos_request(req).await;
move |cx| {
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
app_fn(cx).into_view(cx)
}
};
spawn_blocking({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
async move {
tokio::task::LocalSet::new()
.run_until(async {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
let leptos_req = generate_leptos_request(req).await;
move |cx| {
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
app_fn(cx).into_view(cx)
}
};
let (bundle, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
add_context,
);
let (bundle, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata(cx).into(),
add_context,
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
})
.await;
}
});
}
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
});
generate_response(res_options3, rx).await
@@ -979,53 +932,39 @@ where
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || {
async move {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
let leptos_req = generate_leptos_request(req).await;
move |cx| {
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
app_fn(cx).into_view(cx)
}
};
spawn_blocking({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
async move {
tokio::task::LocalSet::new()
.run_until(async {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
let leptos_req = generate_leptos_request(req).await;
move |cx| {
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
app_fn(cx).into_view(cx)
}
};
let (stream, runtime, scope) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|_| "".into(),
add_context,
);
let (stream, runtime, scope) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|_| "".into(),
add_context,
);
// Extract the value of ResponseOptions from here
let cx = leptos::Scope { runtime, id: scope };
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
// Extract the value of ResponseOptions from here
let cx = leptos::Scope { runtime, id: scope };
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let html = build_async_response(stream, &options, runtime, scope).await;
let html = build_async_response(stream, &options, runtime, scope).await;
let new_res_parts = res_options.0.read().clone();
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write();
*writable = new_res_parts;
let mut writable = res_options2.0.write();
*writable = new_res_parts;
_ = tx.send(html);
})
.await;
}
});
_ = tx.send(html);
}
});
@@ -1052,12 +991,12 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<(String, SsrMode)>
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<(String, SsrMode)>>>);
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
let routes = Routes::default();
let routes_inner = routes.clone();
@@ -1081,17 +1020,26 @@ where
// Axum's Router defines Root routes as "/" not ""
let routes = routes
.into_iter()
.map(|(s, m)| {
if s.is_empty() {
("/".to_string(), m)
.map(|listing| {
let path = listing.path();
if path.is_empty() {
RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)
} else {
(s, m)
listing
}
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![("/".to_string(), Default::default())]
vec![RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)]
} else {
routes
}
@@ -1103,7 +1051,7 @@ pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -1112,7 +1060,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -1121,7 +1069,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
handler: H,
) -> Self
where
@@ -1134,7 +1082,7 @@ impl LeptosRoutes for axum::Router {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -1146,7 +1094,7 @@ impl LeptosRoutes for axum::Router {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -1154,38 +1102,65 @@ impl LeptosRoutes for axum::Router {
IV: IntoView + 'static,
{
let mut router = self;
for (path, mode) in paths.iter() {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
get(render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
))
}
SsrMode::InOrder => {
get(render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
))
}
SsrMode::Async => get(render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)),
},
);
for listing in paths.iter() {
let path = listing.path();
for method in listing.methods() {
router = router.route(
path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
},
);
}
}
router
}
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
handler: H,
) -> Self
where
@@ -1193,9 +1168,33 @@ impl LeptosRoutes for axum::Router {
T: 'static,
{
let mut router = self;
for (path, _) in paths.iter() {
router = router.route(path, get(handler.clone()));
for listing in paths.iter() {
for method in listing.methods() {
router = router.route(
listing.path(),
match method {
leptos_router::Method::Get => get(handler.clone()),
leptos_router::Method::Post => post(handler.clone()),
leptos_router::Method::Put => put(handler.clone()),
leptos_router::Method::Delete => {
delete(handler.clone())
}
leptos_router::Method::Patch => patch(handler.clone()),
},
);
}
}
router
}
}
fn get_leptos_pool() -> LocalPoolHandle {
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();
LOCAL_POOL
.get_or_init(|| {
tokio_util::task::LocalPoolHandle::new(
available_parallelism().map(Into::into).unwrap_or(1),
)
})
.clone()
}

View File

@@ -3,25 +3,10 @@ use leptos::{use_context, RuntimeId, ScopeId};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
pub fn html_parts(
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
fn autoreload(options: &LeptosOptions) -> String {
let site_ip = &options.site_addr.ip().to_string();
let reload_port = options.reload_port;
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
@@ -52,7 +37,25 @@ pub fn html_parts(
leptos_hot_reload::HOT_RELOAD_JS
),
false => "".to_string(),
};
}
}
pub fn html_parts(
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let leptos_autoreload = autoreload(options);
let html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
@@ -72,6 +75,46 @@ pub fn html_parts(
(head, tail)
}
pub fn html_parts_separated(
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let leptos_autoreload = autoreload(options);
let html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
let head = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
{head}
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
(head, tail)
}
pub async fn build_async_response(
stream: impl Stream<Item = String> + 'static,
options: &LeptosOptions,
@@ -86,7 +129,7 @@ pub async fn build_async_response(
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
// in async, we load the meta content *now*, after the suspenses have resolved
let meta = use_context::<MetaContext>(cx);

View File

@@ -16,5 +16,6 @@ leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"

View File

@@ -14,11 +14,12 @@ use http::{header, method::Method, uri::Uri, version::Version, StatusCode};
use hyper::body;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
ssr::*,
*,
};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::{generate_head_metadata, MetaContext};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_router::*;
use parking_lot::RwLock;
use std::{pin::Pin, sync::Arc};
@@ -90,7 +91,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -185,6 +186,7 @@ async fn handle_server_fns_inner(
) -> Result<Response> {
let fn_name = req.params::<String>()?;
let headers = req.headers().clone();
let query = req.query_string().unwrap_or("").to_owned().into();
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking({
move || {
@@ -207,16 +209,21 @@ async fn handle_server_fns_inner(
// Add this so that we can set headers and status of the response
provide_context(cx, ResponseOptions::default());
match server_fn(cx, &req_parts.body).await {
let data = match &server_fn.encoding {
Encoding::Url | Encoding::Cbor => {
&req_parts.body
}
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let res = match (server_fn.trait_obj)(cx, data)
.await
{
Ok(serialized) => {
// If ResponseOptions are set, add the headers and status to the request
let res_options =
use_context::<ResponseOptions>(cx);
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
let accept_header = headers
.get("Accept")
@@ -292,8 +299,15 @@ async fn handle_server_fns_inner(
}
Err(e) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(e.to_string())),
}
.body(Body::from(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
)),
};
// clean up the scope
disposer.dispose();
runtime.dispose();
res
} else {
Response::builder()
.status(StatusCode::BAD_REQUEST)
@@ -385,7 +399,7 @@ where
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
@@ -536,7 +550,7 @@ where
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata(cx).into(),
|cx| generate_head_metadata_separated(cx).1.into(),
add_context,
);
@@ -593,7 +607,7 @@ async fn forward_stream(
) {
let cx = Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
@@ -617,7 +631,7 @@ async fn forward_stream(
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network
@@ -700,7 +714,7 @@ where
let (bundle, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata(cx).into(),
|cx| generate_head_metadata_separated(cx).1.into(),
add_context,
);
@@ -932,12 +946,12 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<(String, SsrMode)>
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<(String, SsrMode)>>>);
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
let routes = Routes::default();
let routes_inner = routes.clone();
@@ -961,17 +975,26 @@ where
// Viz's Router defines Root routes as "/" not ""
let routes = routes
.into_iter()
.map(|(s, m)| {
if s.is_empty() {
("/".to_string(), m)
.map(|listing| {
let path = listing.path();
if path.is_empty() {
RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)
} else {
(s, m)
listing
}
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![("/".to_string(), Default::default())]
vec![RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)]
} else {
routes
}
@@ -983,7 +1006,7 @@ pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
) -> Self
where
@@ -992,7 +1015,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + Clone + Send + Sync + 'static,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
) -> Self
@@ -1001,7 +1024,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_handler<H, O>(
self,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
handler: H,
) -> Self
where
@@ -1014,7 +1037,7 @@ impl LeptosRoutes for Router {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
) -> Self
where
@@ -1026,52 +1049,93 @@ impl LeptosRoutes for Router {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + Clone + Send + Sync + 'static,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static,
{
paths.iter().fold(self, |router, (path, mode)| match mode {
SsrMode::OutOfOrder => router.get(
path,
render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
),
SsrMode::InOrder => router.get(
path,
render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
),
SsrMode::Async => router.get(
path,
render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
),
paths.iter().fold(self, |router, listing| {
let path = listing.path();
let mode = listing.mode();
listing.methods().fold(router, |router, method| match mode {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
})
})
}
fn leptos_routes_with_handler<H, O>(
self,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
handler: H,
) -> Self
where
H: Handler<Request, Output = Result<O>> + Clone,
O: IntoResponse + Send + Sync + 'static,
{
paths
.iter()
.fold(self, |router, (path, _)| router.get(path, handler.clone()))
paths.iter().fold(self, |router, listing| {
listing
.methods()
.fold(router, |router, method| match method {
leptos_router::Method::Get => {
router.get(listing.path(), handler.clone())
}
leptos_router::Method::Post => {
router.post(listing.path(), handler.clone())
}
leptos_router::Method::Put => {
router.put(listing.path(), handler.clone())
}
leptos_router::Method::Delete => {
router.delete(listing.path(), handler.clone())
}
leptos_router::Method::Patch => {
router.patch(listing.path(), handler.clone())
}
})
})
}
}

View File

@@ -13,11 +13,11 @@ cfg-if = "1"
leptos_dom = { workspace = true }
leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_server = { workspace = true, default-features = false }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.14"
server_fn = { workspace = true }
server_fn = { workspace = true, default-features = false }
[dev-dependencies]
leptos = { path = ".", default-features = false }
@@ -36,6 +36,8 @@ hydrate = [
"leptos_reactive/hydrate",
"leptos_server/hydrate",
]
default-tls = ["leptos_server/default-tls", "server_fn/default-tls"]
rustls = ["leptos_server/rustls", "server_fn/rustls"]
ssr = [
"leptos_dom/ssr",
"leptos_macro/ssr",
@@ -93,4 +95,8 @@ skip_feature_sets = [
"serde-lite",
"rkyv",
],
[
"default-tls",
"rustls",
],
]

View File

@@ -0,0 +1,47 @@
use crate::TextProp;
/// A collection of additional HTML attributes to be applied to an element,
/// each of which may or may not be reactive.
#[derive(Default, Clone)]
#[repr(transparent)]
pub struct AdditionalAttributes(pub(crate) Vec<(String, TextProp)>);
impl<I, T, U> From<I> for AdditionalAttributes
where
I: IntoIterator<Item = (T, U)>,
T: Into<String>,
U: Into<TextProp>,
{
fn from(value: I) -> Self {
Self(
value
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}
/// Iterator over additional HTML attributes.
#[repr(transparent)]
pub struct AdditionalAttributesIter<'a>(
std::slice::Iter<'a, (String, TextProp)>,
);
impl<'a> Iterator for AdditionalAttributesIter<'a> {
type Item = &'a (String, TextProp);
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl<'a> IntoIterator for &'a AdditionalAttributes {
type Item = &'a (String, TextProp);
type IntoIter = AdditionalAttributesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
AdditionalAttributesIter(self.0.iter())
}
}

View File

@@ -56,7 +56,7 @@
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite),
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and
//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz)
@@ -141,6 +141,8 @@
//! # }
//! ```
mod additional_attributes;
pub use additional_attributes::*;
pub use leptos_config::{self, get_configuration, LeptosOptions};
#[cfg(not(all(
target_arch = "wasm32",
@@ -150,12 +152,15 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
#[allow(deprecated)]
pub use leptos_dom::{
self, create_node_ref, debug_warn, document, error, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_idle_callback, set_interval,
set_timeout, window_event_listener,
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
@@ -177,7 +182,9 @@ pub use for_loop::*;
pub use show::*;
mod suspense;
pub use suspense::*;
mod text_prop;
mod transition;
pub use text_prop::TextProp;
#[cfg(debug_assertions)]
#[doc(hidden)]
pub use tracing;
@@ -215,3 +222,42 @@ pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
/// }
/// ```
pub type AttributeValue = Box<dyn IntoAttribute>;
#[doc(hidden)]
pub trait Component<P> {}
#[doc(hidden)]
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}
impl<P, F, R> Component<P> for F where F: FnOnce(::leptos::Scope, P) -> R {}
#[doc(hidden)]
pub fn component_props_builder<P: Props>(
_f: &impl Component<P>,
) -> <P as Props>::Builder {
<P as Props>::builder()
}
#[cfg(all(not(doc), feature = "csr", feature = "ssr"))]
compile_error!(
"You have both `csr` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently. `csr` is enabled by \
default on `leptos`, and can be disabled by adding `default-features = \
false` to your `leptos` dependency."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "ssr"))]
compile_error!(
"You have both `hydrate` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "csr"))]
compile_error!(
"You have both `hydrate` and `csr` enabled as features, which may cause \
issues. `csr` is enabled by default on `leptos`, and can be disabled by \
adding `default-features = false` to your `leptos` dependency."
);

View File

@@ -1,6 +1,7 @@
use leptos::component;
use leptos_dom::{Fragment, IntoView};
use leptos_reactive::{create_memo, signal_prelude::*, Scope};
use leptos_reactive::{create_memo, signal_prelude::*, Scope, ScopeDisposer};
use std::{cell::RefCell, rc::Rc};
/// A component that will show its children when the `when` condition is `true`,
/// and show the fallback when it is `false`, without rerendering every time
@@ -45,9 +46,18 @@ where
IV: IntoView,
{
let memoized_when = create_memo(cx, move |_| when());
let prev_disposer = Rc::new(RefCell::new(None::<ScopeDisposer>));
move || match memoized_when.get() {
true => children(cx).into_view(cx),
false => fallback(cx).into_view(cx),
move || {
if let Some(disposer) = prev_disposer.take() {
disposer.dispose();
}
let (view, disposer) =
cx.run_child_scope(|cx| match memoized_when.get() {
true => children(cx).into_view(cx),
false => fallback(cx).into_view(cx),
});
*prev_disposer.borrow_mut() = Some(disposer);
view
}
}

Some files were not shown because too many files have changed in this diff Show More