Compare commits

...

137 Commits

Author SHA1 Message Date
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
martin frances
ddd463748d clippy: less .clone() calls, simpler pointer passing. (#707) 2023-03-19 15:30:12 -04:00
Alexis Fontaine
71ee4cd09d fix: view! macro not compiling with a non-default scope name (#704) 2023-03-19 13:14:47 -04:00
Greg Johnston
08c56f7d6c feat: add a debounce helper for event listeners (#691) 2023-03-19 07:10:56 -04:00
Elliot Waite
e1ba26b62c feat: add request_animation_frame_with_handle and request_idle_callback_with_handle (#698) 2023-03-18 19:09:36 -04:00
Greg Johnston
309f0bf826 fix: ignore view markers in DynChild hydration (closes issue #697) (#703) 2023-03-18 19:01:56 -04:00
Greg Johnston
1698ffa7db fix issues in release mode (closes #700) (#701) 2023-03-18 11:04:06 -04:00
Greg Johnston
556955cf1a docs: beginning work on router docs (#682) 2023-03-18 07:34:43 -04:00
Elliot Waite
a9f778459a examples: remove duplicate console_error_panic_hook::set_once() calls (#692) 2023-03-17 16:27:24 -04:00
Greg Johnston
f2ac412253 feat: support diffing inside component children in hot-reload (#690) 2023-03-17 13:53:53 -04:00
Greg Johnston
3bd52fcc9d fix: hydration errors with <Suspense/> inside components in SSR mode (#688) 2023-03-17 12:46:04 -04:00
Vassil "Vasco" Kolarov
b9bd1e103c examples: added example using Tailwind, CSR (only) and Trunk (#666) 2023-03-17 12:45:49 -04:00
Greg Johnston
55f9081465 fix: allow multiple <Suspense/> on same page during in-order or async rendering (#687) 2023-03-17 12:05:36 -04:00
ryndin32
0bac16dba0 docs: typos (#685) 2023-03-15 16:40:57 -04:00
Brett Etter
a8a9c575b5 Added IntoView for ReadSignal and RwSignal in the stable feature. (#677) 2023-03-15 16:40:22 -04:00
Greg Johnston
15ec855db5 Update README.md 2023-03-15 14:34:18 -04:00
Greg Johnston
b8f79a7e56 fix: suppress spurious hydration warnings for tags in leptos_meta (#684) 2023-03-14 14:17:23 -04:00
Greg Johnston
b988ee85f4 fix: leaking stored values (#683) 2023-03-14 11:06:36 -04:00
Greg Johnston
d6e166f105 CI: add --release checks (#681) 2023-03-13 22:19:10 -04:00
Greg Johnston
53ceca8ff8 feat: maintain order of sources and dependencies (#678) 2023-03-13 20:01:03 -04:00
Brett Etter
f2f9759138 fix: release mode (#679) 2023-03-13 20:00:40 -04:00
Greg Johnston
817152ff39 feat: new reactive system implementation (#637) 2023-03-13 17:58:00 -04:00
Greg Johnston
38daaf3b72 chore: apply cargo machete systematically (#671) 2023-03-13 10:16:20 -04:00
Greg Johnston
666d53e2ba feat: <ActionForm/> improvements (#676) 2023-03-13 10:16:02 -04:00
Greg Johnston
b55e9a9e64 v0.2.3: fix broken stable support (#670) 2023-03-13 07:25:08 -04:00
Greg Johnston
8f94c8e6b1 v0.2.2 (#667) 2023-03-12 14:59:04 -04:00
martin frances
604ba3ff90 clippy: signal_wrappers_read, was using .clone() when copy is available. (#665) 2023-03-12 14:52:13 -04:00
Elliot Waite
2e671887d9 docs: typo fixes and other small changes to the docs (#662) 2023-03-12 14:51:47 -04:00
Greg Johnston
e7d56b76b8 fix: apply patches to all instances of a view, not just the first one (#663) 2023-03-11 16:34:13 -05:00
Greg Johnston
87d5bddb21 fix: text node issue in template macro (#661) 2023-03-11 14:25:38 -05:00
Charles Taylor
9d46b7bcb0 feat: impl Copy & Clone for MaybeSignal (#660) 2023-03-11 13:17:16 -05:00
Greg Johnston
591212a56a feat: add fragment support for hot reloading and fix some stuff (#659) 2023-03-11 07:21:37 -05:00
Ben Wishovich
1a3c1e9e52 feat: provide Request<_> in context for Axum, enabling easier extractor use (#632) 2023-03-10 17:28:32 -05:00
martin frances
94998aa95e chore: cargo machete: leptos_macro - Removed unused crates. (#656) 2023-03-10 09:44:23 -05:00
Greg Johnston
c782017578 feat: impl IntoView for &Fragment (#655) 2023-03-10 09:43:03 -05:00
Pikhosh
d291cdb968 fix: show console error instead warning for error! (#654) 2023-03-10 09:42:44 -05:00
ealmloff
29fb1842a5 feat: make server functions work outside of WASM (#643) 2023-03-09 18:03:57 -05:00
Greg Johnston
b085a6c38e docs: add create_effect chapter (#653) 2023-03-09 18:03:38 -05:00
zack.shen
17c12823db docs: spelling error (#651) 2023-03-09 16:45:35 -05:00
martin frances
81401a738c chore: bumped typed-builder up to 0.14. (#648) 2023-03-09 16:44:27 -05:00
martin frances
c9476af063 chore: bump bytecheck to 0.7, remove deprecated simdutf8_std. (#647)
* bump bytecheck to 0.7, remove deprecated simdutf8_std.

* When using rkyv, must use the appropiate CheckBytes.
2023-03-09 16:44:06 -05:00
Greg Johnston
1f95eb1e1d chore: typo (closes issue #645) (#646) 2023-03-08 19:52:51 -05:00
Vanius Bittencourt
1584ab6b72 feat: refactor leptos_config to allow loading from string (#628) 2023-03-08 19:49:19 -05:00
martin frances
c63c8ac447 chore: cargo machete: Strip down leptos_server. (#644) 2023-03-08 19:37:22 -05:00
martin frances
1a0e2b509e chore: bump serde-wasm-bindgen to 0.5. (#639) 2023-03-08 19:12:30 -05:00
martin frances
c66b673067 chore: <Form/> component Removed unused variables. (#640) 2023-03-07 14:04:55 -05:00
martin frances
a13468228a Bumped tower-http upto 0.4. (#638) 2023-03-07 14:03:54 -05:00
Greg Johnston
bb0324fd48 fix: custom events (closes issue #641) (#642) 2023-03-07 14:00:48 -05:00
jo!
369ea8531f examples: add session_auth_axum (#589) 2023-03-06 21:16:56 -05:00
Greg Johnston
9cc28943aa CI: split into three actions (#636) 2023-03-06 21:00:56 -05:00
erwanvivien
10bdff7d4b de-duplicate todomvc example (#634) 2023-03-06 16:52:35 -05:00
martin frances
27fb430900 bump typed-builder to version 0.13. (#633) 2023-03-06 09:07:21 -05:00
jfloresremar
207dedab6e Update 04_iteration.md (#630) 2023-03-06 09:06:58 -05:00
IchHabeKeineNamen
0052b10df3 docs: fix instruction typos (#631) 2023-03-06 09:05:21 -05:00
Greg Johnston
08d98691a3 fix: boolean attributes in SSR (#629) 2023-03-04 14:24:08 -05:00
WafflePersonThing
34aa0e014b fix: added missing attributes of events that don't bubble (#625)
references used:
- https://developer.mozilla.org/en-US/docs/Web/API/
- web archives of the above before jun 11th 2022, relevant: https://github.com/mdn/content/issues/19590
2023-03-04 11:30:12 -05:00
Greg Johnston
55ce805b60 feat: hot reloading support for cargo-leptos (#592) 2023-03-04 09:04:22 -05:00
Greg Johnston
1e0adcd89a docs: add a chapter on async actions and create_action (#623) 2023-03-03 17:25:19 -05:00
Greg Johnston
4e67b3aef8 CI: exclude rkyv combos with other serialization traits (#622) 2023-03-03 15:48:06 -05:00
Greg Johnston
02e2948e00 fix: suppress warnings caused by resource loading in generate_route_list (closes #582) (#621) 2023-03-03 13:20:38 -05:00
Greg Johnston
bd86125629 feat: allow easier client-side form validation (closes #413) (#620) 2023-03-03 13:19:54 -05:00
Greg Johnston
11d9018e4f docs: add patterns for global state (closes #245) (#619) 2023-03-03 11:51:21 -05:00
Greg Johnston
553e38ba15 tests: use check instead of build in CI for disk space (#616) 2023-03-03 11:50:50 -05:00
Greg Johnston
c8e6d18139 feat: allow multiple class names in view! macro class = (closes #612) (#614) 2023-03-03 10:44:15 -05:00
Greg Johnston
e29f6a884f docs: improve "Getting Started" page (#618) 2023-03-03 10:43:49 -05:00
Greg Johnston
10a2ada42a add note about running Trunk from root 2023-03-03 10:42:18 -05:00
martin frances
2dd9116a20 chore: clippy - simplified conditional logic in transition.rs. (#615) 2023-03-03 09:06:56 -05:00
Roland Fredenhagen
2ee323135f feat: support expressions in #[prop(default=...)] (#611) 2023-03-02 19:15:45 -05:00
Ivan Agafonov
cebc824fbe docs: updated error handling code (#610)
code is from already updated example
2023-03-02 07:24:04 -05:00
Sergei Gnezdov
dd0bcb950a docs: fix compilation error, Issue #608 (#609)
Compiler reports error
F may not live long enough
2023-03-02 07:23:35 -05:00
Greg Johnston
bb5ad101a2 publish framework-independent server_fn crate (#605) 2023-03-02 07:22:36 -05:00
Ivan Agafonov
df90f183fd docs: use create_node_ref instead of NodeRef::new (#607)
Code in the example already updated by someone
2023-03-02 07:22:18 -05:00
ealmloff
0c261c0fb0 feat: make server functions framework agnostic (#596) 2023-03-01 20:56:30 -05:00
Greg Johnston
f46084723a fix: memory leak in streaming SSR (closes issue #590) (#601) 2023-03-01 20:54:28 -05:00
Qwox
08ad6832af fix: set new value before resetting input (#604)
Co-authored-by: Qwox <qwox@qwox.com>
2023-03-01 20:04:37 -05:00
Greg Johnston
700a110350 Merge pull request #603 from makoven/patch-1
Fix typo in 03_components.md
2023-03-01 16:29:46 -05:00
Artem Makoven
33b5d8c4fb Fix typo in 03_components.md 2023-03-02 05:54:59 +09:00
Greg Johnston
ceb7bd398d Merge pull request #602 from iagafonov/patch-1 2023-03-01 14:26:49 -05:00
Ivan Agafonov
7f72c804f4 typo
_cx replaced with cx
2023-03-01 20:08:03 +03:00
Greg Johnston
be0a793179 Merge pull request #599 from leptos-rs/hydration-do-not-reset
fix: SSR + hydration improvements
2023-03-01 06:00:50 -05:00
Greg Johnston
e1625106b8 fix SSR tests 2023-02-28 21:35:59 -05:00
Greg Johnston
abef12279b fix: don't re-set attributes found in HTML during hydration (closes #597) 2023-02-28 19:56:13 -05:00
Greg Johnston
578853877a fix: restore SSR fast-path support 2023-02-28 15:36:52 -05:00
Greg Johnston
04eae63e39 examples: include missing examples in CI (#598) 2023-02-28 15:33:02 -05:00
Brendon Otto
4b98ece2b4 example: update README.md (#595)
Incorrect framework referenced
2023-02-28 09:45:10 -05:00
Greg Johnston
1b2a0fe2ad fix: mouseenter and mouseleave do not bubble (#593) 2023-02-28 09:39:52 -05:00
Thomas Kratz
ab6ddc1194 fix: make counter test compile (#588) 2023-02-27 21:50:12 -05:00
Azz
b153ab51ee feat: support rkyv encoding (#577) 2023-02-26 16:12:53 -05:00
Greg Johnston
34f7b90177 perf: improvements to event delegation and element creation in <For/> (#579) 2023-02-26 08:31:03 -05:00
g-re-g
3648af0d9d fix: correct scheme handling in router, and improve matching code by removing regexes (#569) 2023-02-26 07:15:14 -05:00
Greg Johnston
3d50ca32cd v0.2.0 2023-02-25 15:45:35 -05:00
tanguy-lf
e576d93f83 examples: add ssr_mode_axum (#575) 2023-02-25 11:24:24 -05:00
Greg Johnston
e71779b8a6 fix: <Transition/> with local_resource (closes #562) (#574) 2023-02-24 19:51:03 -05:00
Markus Kohlhase
0301c7f1cf example: Login with API token (CSR only) (#523) 2023-02-24 17:11:58 -05:00
Remo
46e6e7629c chore: macro panic hygiene (#568) 2023-02-24 16:36:05 -05:00
SleeplessOne1917
a985ae5660 fix: <Meta/> component as_ property outputs correct attribute html (#573) 2023-02-24 08:58:15 -05:00
Denis Nazarov
efe29fd057 Relax Eq to PartialEq for create_slice() (#570)
Co-authored-by: Denis Nazarov <denis.nazarov@gmail.com>
2023-02-23 08:00:01 -05:00
Greg Johnston
05b5b25c0e fixes issue #565 (#566) 2023-02-22 09:20:33 -05:00
Greg Johnston
46f6deddbf fix: transition fallback (closes #562) (#563) 2023-02-21 15:11:08 -05:00
Fangdun Tsai
e9c4b490e5 feat: viz integration (#506) 2023-02-21 12:29:15 -05:00
PolarMutex
f7d0eea04d feature: add class prop to <Html/> component (#554) 2023-02-21 12:28:11 -05:00
Greg Johnston
bf246a62e7 fix: issue with local resources blocking <Suspense/> fragments from resolving (#561) 2023-02-21 06:29:29 -05:00
Greg Johnston
b8acfd758d fix: remove unnecessary log (#560) 2023-02-20 21:34:09 -05:00
Greg Johnston
dc8fd37461 docs: add create_resource, <Suspense/>, and <Transition/> (#559) 2023-02-20 17:39:17 -05:00
Greg Johnston
cc2b310e4c docs: add example of <ButtonC on:click/> syntax (#558) 2023-02-20 15:41:56 -05:00
Thomas Versteeg
884348d7f8 doc: fix button name in parent_child example (#555) 2023-02-20 14:53:04 -05:00
Greg Johnston
dafd6e51c5 v0.2.0-beta (#557) 2023-02-20 14:52:31 -05:00
Ben Wishovich
322041917d fix issue with redirects in server fns creating multiple Location headers (#550) 2023-02-20 08:55:47 -05:00
Ikko Eltociear Ashimine
a2eaf9b3ee fix: typo in hydration docs(#552)
identifer -> identifier
2023-02-20 07:09:33 -05:00
Chrislearn Young
4032bfc210 fix: document docs typo (#553) 2023-02-20 07:08:51 -05:00
Greg Johnston
4ff08f042b change: pass Scope as argument into Resource::read() and Resource::with() (#542) 2023-02-19 19:52:31 -05:00
Greg Johnston
ce4b0ecbe1 fix: more work on hydration IDs with <Suspense/> (#545) 2023-02-18 21:20:40 -05:00
Greg Johnston
6c31d09eb2 revert PR #538 (#544) 2023-02-18 18:39:08 -05:00
Greg Johnston
59ad6a4725 revert accident 2023-02-18 16:03:20 -05:00
Greg Johnston
884dacbc6c fix example 2023-02-18 16:02:19 -05:00
Dmitrii Kuzmin
9c572f7617 fix(examples): hackernews_axum styles href (#536) 2023-02-18 15:17:54 -05:00
jquesada2016
487dba90d8 fix: off-by-one error in <For/> (closes #533) (#538) 2023-02-18 14:57:14 -05:00
Greg Johnston
20f24d2f3a fix: building leptos_reactive in release mode (#540) 2023-02-18 12:45:58 -05:00
Greg Johnston
20cbc240ee v0.2.0-alpha2 (#539) 2023-02-18 12:45:46 -05:00
jquesada2016
f2f52b2533 change: move signal method implementations into traits in signal prelude (#490) 2023-02-18 07:30:03 -05:00
Sean Aye
46d6e3f78c fix compile of leptos dom (#535) 2023-02-17 18:25:58 -05:00
Greg Johnston
586f524015 feature: in-order streaming and async rendering (#496) 2023-02-17 17:31:32 -05:00
Greg Johnston
79781ec20c Fix test import location 2023-02-17 16:18:22 -05:00
Greg Johnston
91f6d9a404 What's in a name? 2023-02-17 07:25:42 -05:00
Greg Johnston
76a74ecde2 fix: hydration IDs for elements following <Suspense/> (closes #527) (#531) 2023-02-16 21:12:52 -05:00
Greg Johnston
0071a48b8a feature: reintroduce limited template-node cloning w/ template macro (#526) 2023-02-16 07:02:01 -05:00
Greg Johnston
8d42e91eb8 fix: top-level SVG in view macro with new exports (#525) 2023-02-15 15:38:06 -05:00
281 changed files with 17770 additions and 3704 deletions

45
.github/workflows/check-examples.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make check-examples

45
.github/workflows/check-stable.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- stable
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make check-stable

45
.github/workflows/check.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Run `cargo check` ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all libraries
run: cargo make check

34
.github/workflows/fmt.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Run rustfmt
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Run Rustfmt
run: cargo fmt -- --check

View File

@@ -11,7 +11,7 @@ env:
jobs:
test:
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -39,10 +39,7 @@ jobs:
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- name: Run Rustfmt
run: cargo fmt -- --check
- uses: Swatinem/rust-cache@v2
- name: Run tests with all features
run: cargo make ci
run: cargo make test

View File

@@ -4,13 +4,19 @@ members = [
"leptos",
"leptos_dom",
"leptos_config",
"leptos_hot_reload",
"leptos_macro",
"leptos_reactive",
"leptos_server",
"server_fn",
"server_fn_macro",
"server_fn/server_fn_macro_default",
# integrations
"integrations/actix",
"integrations/axum",
"integrations/viz",
"integrations/utils",
# libraries
"meta",
@@ -19,17 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.2.0-alpha"
version = "0.2.3"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.2.0-alpha" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.0-alpha" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.0-alpha" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.0-alpha" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.0-alpha" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.0-alpha" }
leptos_router = { path = "./router", version = "0.2.0-alpha" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.0-alpha" }
leptos = { path = "./leptos", default-features = false, version = "0.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" }
[profile.release]
codegen-units = 1

View File

@@ -7,42 +7,65 @@
# make tasks run at the workspace root
default_to_workspace = false
[tasks.ci]
dependencies = ["build", "check-examples", "test"]
[tasks.build]
[tasks.check]
clear = true
dependencies = ["build-all", "build-wasm"]
dependencies = [
"check-all",
"check-wasm",
"check-all-release",
"check-wasm-release",
]
[tasks.build-all]
[tasks.check-all]
command = "cargo"
args = ["+nightly", "build-all-features"]
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.build-wasm]
[tasks.check-wasm]
clear = true
dependencies = [{ name = "build-wasm", path = "leptos" }]
dependencies = [{ name = "check-wasm", path = "leptos" }]
[tasks.check-all-release]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-wasm-release]
clear = true
dependencies = [{ name = "check-wasm-release", path = "leptos" }]
[tasks.check-examples]
clear = true
dependencies = [
{ name = "check", path = "examples/counter" },
{ name = "check", path = "examples/counter_isomorphic" },
{ name = "check", path = "examples/counter_without_macros" },
{ name = "check", path = "examples/counters" },
{ name = "check", path = "examples/counters_stable" },
{ name = "check", path = "examples/error_boundary" },
{ name = "check", path = "examples/errors_axum" },
{ name = "check", path = "examples/fetch" },
{ name = "check", path = "examples/hackernews" },
{ name = "check", path = "examples/hackernews_axum" },
{ name = "check", path = "examples/login_with_token_csr_only" },
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
{ name = "check", path = "examples/session_auth_axum" },
{ name = "check", path = "examples/ssr_modes" },
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/tailwind_csr_trunk" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todo_app_sqlite_viz" },
{ name = "check", path = "examples/todomvc" },
]
[tasks.check-stable]
clear = true
dependencies = [
{ name = "check", path = "examples/counter_without_macros" },
{ name = "check", path = "examples/counters_stable" },
]
[tasks.test]
clear = true
dependencies = ["test-all"]

View File

@@ -78,7 +78,7 @@ rustup target add wasm32-unknown-unknown
If youre on `stable`, note the following:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0", features = ["stable"] }`
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`,
youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
@@ -99,6 +99,10 @@ Open browser on [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.
### Is it production ready?
People usually mean one of three things by this question.

View File

@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
l021 = { package = "leptos", version = "0.2.1" }
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
@@ -27,4 +28,4 @@ features = [
"Document",
"HtmlElement",
"HtmlInputElement"
]
]

View File

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

View File

@@ -1,35 +1,114 @@
use std::{cell::Cell, rc::Rc};
use test::Bencher;
use std::{cell::Cell, rc::Rc};
#[bench]
fn leptos_create_1000_signals(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
fn leptos_deep_creation(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_deep_update(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_narrowing_down(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
fn leptos_fanning_out(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(|cx| {
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_narrowing_update(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
@@ -48,17 +127,20 @@ fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_scope, create_signal};
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope({
create_scope(runtime, {
let acc = Rc::clone(&acc);
move |cx| {
let (r, w) = create_signal(cx, 0);
@@ -76,16 +158,183 @@ fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
disposer.dispose();
}
});
runtime.dispose();
}
#[bench]
fn sycamore_create_1000_signals(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
fn l021_deep_creation(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_deep_update(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_narrowing_down(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_fanning_out(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_narrowing_update(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
}
});
assert_eq!(acc.get(), 499500);
writes[1].update(|n| *n += 1);
writes[10].update(|n| *n += 1);
writes[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo(), 499503);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope(runtime, {
let acc = Rc::clone(&acc);
move |cx| {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r());
}
});
w.update(|n| *n += 1);
}
})
})
.collect::<Vec<_>>();
for disposer in disposers {
disposer.dispose();
}
});
runtime.dispose();
}
#[bench]
fn sycamore_narrowing_down(b: &mut Bencher) {
use sycamore::reactive::{
create_effect, create_memo, create_scope, create_signal,
};
b.iter(|| {
let d = create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
let sigs = Rc::new(
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
);
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
@@ -97,13 +346,80 @@ fn sycamore_create_1000_signals(b: &mut Bencher) {
}
#[bench]
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
fn sycamore_fanning_out(b: &mut Bencher) {
use sycamore::reactive::{
create_effect, create_memo, create_scope, create_signal,
};
b.iter(|| {
let d = create_scope(|cx| {
let sig = create_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 1000);
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_deep_creation(b: &mut Bencher) {
use sycamore::reactive::*;
b.iter(|| {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));
}
}
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_deep_update(b: &mut Bencher) {
use sycamore::reactive::*;
b.iter(|| {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));
}
}
signal.set(1);
assert_eq!(*memos[999].get(), 1001);
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_narrowing_update(b: &mut Bencher) {
use sycamore::reactive::{
create_effect, create_memo, create_scope, create_signal,
};
b.iter(|| {
let d = create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
let sigs = Rc::new(
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
);
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
@@ -129,7 +445,7 @@ fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
}
#[bench]
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
fn sycamore_scope_creation_and_disposal(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_scope, create_signal};
b.iter(|| {

View File

@@ -1,13 +1,13 @@
# Getting Started
There are two basic paths to getting started with Leptos:
1. Client-side rendering with [Trunk](https://trunkrs.dev/)
2. Full-stack rendering with [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos)
For the early examples, it will be easiest to begin with Trunk. Well introduce
`cargo-leptos` a little later in this series.
If you dont already have it installed, you can install Trunk by running
```bash
@@ -20,12 +20,22 @@ Create a basic Rust binary project
cargo init leptos-tutorial
```
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
> We recommend using `nightly` Rust, as it enables [a few nice features](https://github.com/leptos-rs/leptos#nightly-note). To use `nightly` Rust with WebAssembly, you can run
>
> ```bash
> rustup toolchain install nightly
> rustup default nightly
> rustup target add wasm32-unknown-unknown
> ```
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos
```
Create a simple `index.html` in the root of the `leptos-tutorial` directory
```html
<!DOCTYPE html>
<html>
@@ -35,14 +45,26 @@ Create a simple `index.html` in the root of the `leptos-tutorial` directory
```
And add a simple “Hello, world!” to your `main.rs`
```rust
```rust
use leptos::*;
fn main() {
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
}
```
Now run `trunk serve --open`. Trunk should automatically compile your app and
open it in your default browser. If you make edits to `main.rs`, Trunk will
recompile your source code and live-reload the page.
Your directory structure should now look something like this
```
leptos_tutorial
├── src
│ └── main.rs
├── Cargo.toml
├── index.html
```
Now run `trunk serve --open` from the root of the `leptos-tutorial` directory.
Trunk should automatically compile your app and open it in your default browser.
If you make edits to `main.rs`, Trunk will recompile your source code and
live-reload the page.

View File

@@ -0,0 +1,110 @@
# Responding to Changes with `create_effect`
Believe it or not, weve made it this far without having mentioned half of the reactive system: effects.
Leptos is built on a fine-grained reactive system, which means that individual reactive values (“signals,” sometimes known as observables) trigger rerunning the code that reacts to them (“effects,” sometimes known as observers). These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as theres no observable value to subscribe to.
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
```rust
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
create_effect(cx, move |_| {
// immediately prints "Value: 0" and subscribes to `a`
log::debug!("Value: {}", a());
});
```
The effect function is called with an argument containing whatever value it returned the last time it ran. On the initial run, this is `None`.
By default, effects **do not run on the server**. This means you can call browser-specific APIs within the effect function without causing issues. If you need an effect to run on the server, use [`create_isomorphic_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_isomorphic_effect.html).
## Autotracking and Dynamic Dependencies
If youre familiar with a framework like React, you might notice one key difference. React and similar frameworks typically require you to pass a “dependency array,” an explicit set of variables that determine when the effect should rerun.
Because Leptos comes from the tradition of synchronous reactive programming, we dont need this explicit dependency list. Instead, we automatically track dependencies depending on which signals are accessed within the effect.
This has two effects (no pun intended). Dependencies are
1. **Automatic**: You dont need to maintain a dependency list, or worry about what should or shouldnt be included. The framework simply tracks which signals might cause the effect to rerun, and handles it for you.
2. **Dynamic**: The dependency list is cleared and updated every time the effect runs. If your effect contains a conditional (for example), only signals that are used in the current branch are tracked. This means that effects rerun the absolute minimum number of times.
> If this sounds like magic, and if you want a deep dive into how automatic dependency tracking works, [check out this video](https://www.youtube.com/watch?v=GWB3vTWeLd4). (Apologies for the low volume!)
## Effects as Zero-Cost-ish Abstraction
While theyre not a “zero-cost abstraction” in the most technical sense—they require some additional memory use, exist at runtime, etc.—at a higher level, from the perspective of whatever expensive API calls or other work youre doing within them, effects are a zero-cost abstraction. They rerun the absolute minimum number of times necessary, given how youve described them.
Imagine that Im creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
```rust
let (first, set_first) = create_signal(cx, String::new());
let (last, set_last) = create_signal(cx, String::new());
let (use_last, set_use_last) = create_signal(cx, true);
// this will add the name to the log
// any time one of the source signals changes
create_effect(cx, move |_| {
log(
cx,
if use_last() {
format!("{} {}", first(), last())
} else {
first()
},
)
});
```
If `use_last` is `true`, effect should rerun whenever `first`, `last`, or `use_last` changes. But if I toggle `use_last` to `false`, a change in `last` will never cause the full name to change. In fact, `last` will be removed from the dependency list until `use_last` toggles again. This saves us from sending multiple unnecessary requests to the API if I change `last` multiple times while `use_last` is still `false`.
## To `create_effect`, or not to `create_effect`?
Effects are intended to run _side-effects_ of the system, not to synchronize state _within_ the system. In other words: dont write to signals within effects.
If you need to define a signal that depends on the value of other signals, use a derived signal or [`create_memo`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_memo.html).
If you need to synchronize some reactive value with the non-reactive world outside—like a web API, the console, the filesystem, or the DOM—create an effect.
> If youre curious for more information about when you should and shouldnt use `create_effect`, [check out this video](https://www.youtube.com/watch?v=aQOFJQ2JkvQ) for a more in-depth consideration!
## Effects and Rendering
Weve managed to get this far without mentioning effects because theyre built into the Leptos DOM renderer. Weve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
```rust
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<p>{count}</p>
}
```
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
```rust
let (count, set_count) = create_signal(cx, 0);
// create a DOM element
let p = create_element("p");
// create an effect to reactively update the text
create_effect(cx, move |prev_value| {
// first, access the signals value and convert it to a string
let text = count().to_string();
// if this is different from the previous value, update the node
if prev_value != Some(text) {
p.set_text_content(&text);
}
// return this value so we can memoize the next update
text
});
```
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
<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>

View File

@@ -0,0 +1,171 @@
# Global State Management
So far, we've only been working with local state in components
We've only seen how to communicate between parent and child components
But there are also more general ways to manage global state
The three best approaches to global state are
1. Using the router to drive global state via the URL
2. Passing signals through context
3. Creating a global state struct and creating lenses into it with `create_slice`
## Option #1: URL as Global State
The next few sections of the tutorial will be about the router.
So for now, we'll just look at options #2 and #3.
## Option #2: Passing Signals through Context
In virtual DOM libraries like React, using the Context API to manage global
state is a bad idea: because the entire app exists in a tree, changing
some value provided high up in the tree can cause the whole app to render.
In fine-grained reactive libraries like Leptos, this is simply not the case.
You can create a signal in the root of your app and pass it down to other
components using provide_context(). Changing it will only cause rerendering
in the specific places it is actually used, not the whole app.
We start by creating a signal in the root of the app and providing it to
all its children and descendants using `provide_context`.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
// here we create a signal in the root that can be consumed
// anywhere in the app.
let (count, set_count) = create_signal(cx, 0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(cx, count);
view! { cx,
// SetterButton is allowed to modify the count
<SetterButton set_count/>
// These consumers can only read from it
// But we could give them write access by passing `set_count` if we wanted
<FancyMath/>
<ListItems/>
}
}
```
`<SetterButton/>` is the kind of counter weve written several times now.
(See the sandbox below if you dont understand what I mean.)
`<FancyMath/>` and `<ListItems/>` both consume the signal were providing via
`use_context` and do something with it.
```rust
/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath(cx: Scope) -> impl IntoView {
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>(cx)
// we know we just provided this in the parent component
.expect("there to be a `count` signal provided");
let is_even = move || count() & 1 == 0;
view! { cx,
<div class="consumer blue">
"The number "
<strong>{count}</strong>
{move || if is_even() {
" is"
} else {
" is not"
}}
" even."
</div>
}
}
```
This kind of “provide a signal in a parent, consume it in a child” should be familiar
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
pattern you use to communicate between parents and children works for grandparents and
grandchildren, or any ancestors and descendents: in other words, between “global” state
in the root component of your app and any other components anywhere else in the app.
Because of the fine-grained nature of updates, this is usually all you need. However,
in some cases with more complex state changes, you may want to use a slightly more
structured approach to global state.
## Option #3: Create a Global State Struct
You can use this approach to build a single global data structure
that holds the state for your whole app, and then access it by
taking fine-grained slices using
[`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html)
or [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html),
so that changing one part of the state doesn't cause parts of your
app that depend on other parts of the state to change.
You can begin by defining a simple state struct:
```rust
#[derive(Default, Clone, Debug)]
struct GlobalState {
count: u32,
name: String,
}
```
Provide it in the root of your app so its available everywhere.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
// we'll provide a single signal that holds the whole state
// each component will be responsible for creating its own "lens" into it
let state = create_rw_signal(cx, GlobalState::default());
provide_context(cx, state);
// ...
```
Then child components can access “slices” of that state with fine-grained
updates via `create_slice`. Each slice signal only updates when the particular
piece of the larger struct it accesses updates. This means you can create a single
root signal, and then take independent, fine-grained slices of it in different
components, each of which can update without notifying the others of changes.
```rust
/// A component that updates the count in the global state.
#[component]
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
// `create_slice` lets us create a "lens" into the data
let (count, set_count) = create_slice(
cx,
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data
|state| state.count,
// our setter describes how to mutate that slice, given a new value
|state, n| state.count = n,
);
view! { cx,
<div class="consumer blue">
<button
on:click=move |_| {
set_count(count() + 1);
}
>
"Increment Global Count"
</button>
<br/>
<span>"Count is: " {count}</span>
</div>
}
}
```
Clicking this button only updates `state.count`, so if we create another slice
somewhere else that only takes `state.name`, clicking the button 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">

View File

@@ -14,24 +14,29 @@
- [Passing Children to Components](./view/09_component_children.md)
- [Interlude: Reactivity and Functions](./interlude_functions.md)
- [Testing](./testing.md)
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Async]()
- [Resource]()
- [Suspense]()
- [Transition]()
- [State Management]()
- [Interlude: Advanced Reactivity]()
- [Router]()
- [Fundamentals]()
- [defining `<Routes/>`]()
- [`<A/>`]()
- [Async](./async/README.md)
- [Loading Data with Resources](./async/10_resources.md)
- [Suspense](./async/11_suspense.md)
- [Transition](./async/12_transition.md)
- [Actions](./async/13_actions.md)
- [Responding to Changes with `create_effect`](./14_create_effect.md)
- [Global State Management](./15_global_state.md)
- [Router](./router/README.md)
- [Defining `<Routes/>`](./router/16_routes.md)
- [Nested Routing](./router/17_nested_routing.md)
- [Params and Queries](./router/18_params_and_queries.md)
- [`<A/>`](./router/19_a.md)
- [`<Form/>`]()
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Metadata]()
- [SSR]()
- [Models of SSR]()
- [`cargo-leptos`]()
- [Hydration Footguns]()
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
- [Headers]()
- [Cookies]()
- [Server Functions]()
@@ -39,3 +44,5 @@
- [Forms]()
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size]()

View File

@@ -0,0 +1,53 @@
# Loading Data with Resources
A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if its still pending.
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments (other than the ubiquitous `cx`):
1. a source signal, which will generate a new `Future` whenever it changes
2. a fetcher function, which takes the data from that signal and returns a `Future`
Heres an example
```rust
// our source signal: some synchronous, local state
let (count, set_count) = create_signal(cx, 0);
// our resource
let async_data = create_resource(cx,
count,
// every time `count` changes, this will run
|value| async move {
log!("loading data from API");
load_data(value).await
},
);
```
To create a resource that simply runs once, you can pass a non-reactive, empty source signal:
```rust
let once = create_resource(cx, || (), |_| async move { load_data().await });
```
To access the value you can use `.read(cx)` or `.with(cx, |data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but with two differences
1. For any `Resource<_, T>`, they always return `Option<T>`, not `T`: because its always possible that your resource is still loading.
2. They take a `Scope` argument. Youll see why in the next chapter, on `<Suspense/>`.
So, you can show the current state of a resource in your view:
```rust
let once = create_resource(cx, || (), |_| async move { load_data().await });
view! { cx,
<h1>"My Data"</h1>
{move || match once.read(cx) {
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
}}
}
```
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
<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>

View File

@@ -0,0 +1,72 @@
# `<Suspense/>`
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
```rust
let (count, set_count) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
view! { cx,
<h1>"My Data"</h1>
{move || match once.read(cx) {
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
}}
}
```
But what if we have two resources, and want to wait for both of them?
```rust
let (count, set_count) = create_signal(cx, 0);
let (count2, set_count2) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
view! { cx,
<h1>"My Data"</h1>
{move || match (a.read(cx), b.read(cx)) {
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
(Some(a), Some(b)) => view! { cx,
<ShowA a/>
<ShowA b/>
}.into_view(cx)
}}
}
```
Thats not _so_ bad, but its kind of annoying. What if we could invert the flow of control?
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If its still waiting for resources to load, it shows the `fallback`. When theyve all loaded, it shows the children.
```rust
let (count, set_count) = create_signal(cx, 0);
let (count2, set_count2) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
view! { cx,
<h1>"My Data"</h1>
<Suspense
fallback=move || view! { cx, <p>"Loading..."</p> }
>
<h2>"My Data"</h2>
<h3>"A"</h3>
{move || {
a.read(cx)
.map(|a| view! { cx, <ShowA a/> })
}}
<h3>"B"</h3>
{move || {
b.read(cx)
.map(|b| view! { cx, <ShowB b/> })
}}
</Suspense>
}
```
Every time one of the resources is reloading, the `"Loading..."` fallback will show again.
This inversion of the flow of control makes it easier to add or remove individual resources, as you 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/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,9 @@
# `<Transition/>`
Youll notice in the `<Suspense/>` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, theres [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html).
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. 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>

View File

@@ -0,0 +1,94 @@
# Mutating Data with Actions
Weve talked about how to load `async` data with resources. Resources immediately load data and work closely with `<Suspense/>` and `<Transition/>` components to show whether data is loading in your app. But what if you just want to call some arbitrary `async` function and keep track of what its doing?
Well, you could always use [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html). This allows you to just spawn an `async` task in a synchronous environment by handing the `Future` off to the browser (or, on the server, Tokio or whatever other runtime youre using). But how do you know if its still pending? Well, you could just set a signal to show whether its loading, and another one to show the result...
All of this is true. Or you could use the final `async` primitive: [`create_action`](https://docs.rs/leptos/latest/leptos/fn.create_action.html).
Actions and resources seem similar, but they represent fundamentally different things. If youre trying to load data by running an `async` function, either once or when some other value changes, you probably want to use `create_resource`. If youre trying to occasionally run an `async` function in response to something like a user clicking a button, you probably want to use `create_action`.
Say we have some `async` function we want to run.
```rust
async fn add_todo(new_title: &str) -> Uuid {
/* do some stuff on the server to add a new todo */
}
```
`create_action` takes a reactive `Scope` and an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
> The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
>
> ```rust
> // if there's a single argument, just use that
> let action1 = create_action(cx, |input: &String| {
> let input = input.clone();
> async move { todo!() }
> });
>
> // if there are no arguments, use the unit type `()`
> let action2 = create_action(cx, |input: &()| async { todo!() });
>
> // if there are multiple arguments, use a tuple
> let action3 = create_action(cx,
> |input: &(usize, String)| async { todo!() }
> );
> ```
>
> Because the action function takes a reference but the `Future` needs to have a `'static` lifetime, youll usually need to clone the value to pass it into the `Future`. This is admittedly awkward but it unlocks some powerful features like optimistic UI. Well see a little more about that in future chapters.
So in this case, all we need to do to create an action is
```rust
let add_todo = create_action(cx, |input: &String| {
let input = input.to_owned();
async move { add_todo(&input).await }
});
```
Rather than calling `add_todo` directly, well call it with `.dispatch()`, as in
```rust
add_todo.dispatch("Some value".to_string());
```
You can do this from an event listener, a timeout, or anywhere; because `.dispatch()` isnt an `async` function, it can be called from a synchronous context.
Actions provide access to a few signals that synchronize between the asynchronous action youre calling and the synchronous reactive system:
```rust
let submitted = add_todo.input(); // RwSignal<Option<String>>
let pending = add_todo.pending(); // ReadSignal<bool>
let todo_id = add_todo.value(); // RwSignal<Option<Uuid>>
```
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
```rust
let input_ref = create_node_ref::<Input>(cx);
view! { cx,
<form
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
let input = input_ref.get().expect("input to exist");
add_todo.dispatch(input.value());
}
>
<label>
"What do you need to do?"
<input type="text"
node_ref=input_ref
/>
</label>
<button type="submit">"Add Todo"</button>
</form>
// use our loading state
<p>{move || pending().then("Loading...")}</p>
}
```
Now, 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>

View File

@@ -0,0 +1,9 @@
# Working with `async`
So far weve only been working with synchronous users interfaces: You provide some input,
the app immediately process it and updates the interface. This is great, but is a tiny
subset of what web applications do. In particular, most web apps have to deal with some kind
of asynchronous data loading, usually loading something from an API.
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
In this chapter, well see how Leptos helps smooth out that process for you.

View File

@@ -12,19 +12,19 @@ let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
let count_is_odd = move || count() & 1 == 1;
let text = move || if count_is_odd() {
"odd"
"odd"
} else {
"even"
"even"
};
// an effect automatically tracks the signals it depends on
// and re-runs when they change
// and reruns when they change
create_effect(cx, move |_| {
log!("text = {}", text());
log!("text = {}", text());
});
view! { cx,
<p>{move || text().to_uppercase()}</p>
<p>{move || text().to_uppercase()}</p>
}
```
@@ -45,7 +45,7 @@ The key phrase here is “runs some kind of code.” The natural way to “run s
1. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM
2. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the components state
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that re-run
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that rerun
Thats what all our components are doing.
@@ -59,16 +59,16 @@ pub fn SimpleCounter(cx: Scope) -> impl IntoView {
let increment = move |_| set_value.update(|value| *value += 1);
view! { cx,
<button on:click=increment>
{value}
</button>
<button on:click=increment>
{value}
</button>
}
}
```
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
Closures are key to reactivity. They provide the framework with the ability to re-run the smallest possible unit of your application in responsive to a change.
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in responsive to a change.
So remember two things:

View File

@@ -0,0 +1,101 @@
# Defining Routes
## Getting Started
Its easy to get started with the router.
First things first, make sure youve added the `leptos_router` package to your dependencies.
> Its important that the router is a separate package from `leptos` itself. This means that everything in the router can be defined in user-land code. If you want to create your own router, or use no router, youre completely free to do that!
And import the relevant types from the router, either with something like
```rust
use leptos_router::{Route, RouteProps, Router, RouterProps, Routes, RoutesProps};
```
or simply
```rust
use leptos_router::*;
```
## Providing the `<Router/>`
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
> You shouldnt try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
Lets start with a simple `<App/>` component using the router:
```rust
use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! {
<Router>
<nav>
/* ... */
</nav>
<main>
/* ... */
</main>
</Router>
}
}
```
## Defining `<Routes/>`
The [`<Routes/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Route.html) component.
You should place the `<Routes/>` component at the location within your app where you want routes to be rendered. Everything outside `<Routes/>` will be present on every page, so you can leave things like a navigation bar or menu outside the `<Routes/>`.
```rust
use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! {
<Router>
<nav>
/* ... */
</nav>
<main>
// all our routes will appear inside <main>
<Routes>
/* ... */
</Routes>
</main>
</Router>
}
}
```
Individual routes are defined by providing children to `<Routes/>` with the `<Route/>` component. `<Route/>` takes a `path` and a `view`. When the current location matches `path`, the `view` will be created and displayed.
The `path` can include
- a static path (`/users`),
- dynamic, named parameters beginning with a colon (`/:id`),
- and/or a wildcard beginning with an asterisk (`/user/*any`)
The `view` is a function that takes a `Scope` and returns a view.
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home/> }/>
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
</Routes>
```
> The router scores each route to see how good a match it is, so you can define your routes in any order.
Now if you navigate to `/` or to `/users` youll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` youll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
Simple enough?

View File

@@ -0,0 +1,170 @@
# Nested Routing
We just defined the following set of routes:
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home /> }/>
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
</Routes>
```
Theres a certain amount of duplication here: `/users` and `/users/:id`. This is fine for a small app, but you can probably already tell it wont scale well. Wouldnt it be nice if we could nest these routes?
Well... you can!
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home /> }/>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
</Route>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
</Routes>
```
But wait. Weve just subtly changed what our application does.
The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if theres anything you dont understand.
# Nested Routes as Layout
Nested routes are a form of layout, not a method of route definition.
Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple `<Route/>`s on the page at the same time, side by side.
Lets look back at our practical example.
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
</Routes>
```
This means:
- If I go to `/users`, I get the `<Users/>` component.
- If I go to `/users/3`, I get the `<UserProfile/>` component (with the parameter `id` set to `3`; more on that later)
Lets say I use nested routes instead:
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
</Route>
</Routes>
```
This means:
- If I go to `/users/3`, the path matches two `<Route/>`s: `<Users/>` and `<UserProfile/>`.
- If I go to `/users`, the path is not matched.
I actually need to add a fallback route
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="" view=|cx| view! { cx, <NoUser /> }/>
</Route>
</Routes>
```
Now:
- If I go to `/users/3`, the path matches `<Users/>` and `<UserProfile/>`.
- If I go to `/users`, the path matches `<Users/>` and `<NoUser/>`.
When I use nested routes, in other words, each **path** can match multiple **routes**: each URL can render the views provided by multiple `<Route/>` components, at the same time, on the same page.
This may be counter-intuitive, but its very powerful, for reasons youll hopefully see in a few minutes.
## Why Nested Routing?
Why bother with this?
Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like `/contacts/greg`, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If theres no contact selected, maybe you want to show a little instructional text.
You can easily define this with nested routes
```rust
<Routes>
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }/>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
}/>
</Route>
</Routes>
```
You can go even deeper. Say you want to have tabs for each contacts address, email/phone, and your conversations with them. You can add _another_ set of nested routes inside `:id`:
```rust
<Routes>
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
<Route path="address" view=|cx| view! { cx, <Address/> }/>
<Route path="messages" view=|cx| view! { cx, <Messages/> }/>
</Route>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
}/>
</Route>
</Routes>
```
> The main page of the [Remix website](https://remix.run/), a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice.
## `<Outlet/>`
Parent routes do not automatically render their nested routes. After all, they are just components; they dont know exactly where they should render their children, and “just stick at at the end of the parent component” is not a great answer.
Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things:
- if there is no nested route that has been matched, it shows nothing
- if there is a nested route that has been matched, it shows its `view`
Thats all! But its important to know and to remember, because its a common source of “Why isnt this working?” frustration. If you dont provide an `<Outlet/>`, the nested route wont be displayed.
```rust
#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
let contacts = todo!();
view! { cx,
<div style="display: flex">
// the contact list
<For each=contacts
key=|contact| contact.id
view=|cx, contact| todo!()
>
// the nested child, if any
// dont forget this!
<Outlet/>
</div>
}
}
```
## Nested Routing and Performance
All of this is nice, conceptually, but again—whats the big deal?
Performance.
In a fine-grained reactive library like Leptos, its always important to do the least amount of rendering work you can. Because were working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy.
Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the `<ContactList/>` should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of `<ContactList/>`, navigating from Greg to Alice to Bob wont clear the search.
In fact, in this case, we dont even need to rerender the `<Contact/>` component when moving between contacts. The router will just reactively update the `:id` parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, well update single text nodes to change the contacts name, address, and so on, without doing _any_ additional rerendering.
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple 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>

View File

@@ -0,0 +1,77 @@
# Params and Queries
Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point.
There are two ways you can do this:
1. named route **params** like `id` in `/users/:id`
2. named route **queries** like `q` in `/search?q=Foo`
Because of the way URLs are built, you can access the query from _any_ `<Route/>` view. You can access route params from the `<Route/>` that defines them or any of its nested children.
Accessing params and queries is pretty simple with a couple of hooks:
- [`use_query`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
- [`use_params`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
Each of these comes with a typed option (`use_query` and `use_params`) and an untyped option (`use_query_map` and `use_params_map`).
The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.2.3/leptos_router/trait.Params.html) trait on a struct.
> `Params` is a very lightweight trait to convert a flat key-value map of strings into a struct by applying `FromStr` to each field. Because of the flat structure of route params and URL queries, its significantly less flexible than something like `serde`; it also adds much less weight to your binary.
```rust
use leptos::*;
use leptos_router::*;
#[derive(Params)]
struct ContactParams {
id: usize
}
#[derive(Params)]
struct ContactSearch {
q: String
}
```
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure youre importing the right one for the derive macro.
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
The typed versions return `Memo<Result<T>, _>`. Its a Memo so it reacts to changes in the URL. Its a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
```rust
let params = use_params::<ContactParams>(cx);
let query = use_query::<ContactSearch>(cx);
// id: || -> usize
let id = move || {
params.with(|params| {
params
.map(|params| params.id)
.unwrap_or_default()
})
};
```
The untyped versions return `Memo<ParamsMap>`. Again, 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);
// id: || -> Option<String>
let id = move || {
params.with(|params| params.get("id").cloned())
};
```
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But its worth doing this for two reasons:
1. Its correct, i.e., it forces you to consider the cases, “What if the user doesnt pass a value for this query field? What if they pass an invalid value?”
2. Its performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replacing or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we 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>

View File

@@ -0,0 +1,21 @@
# The `<A/>` Component
Client-side navigation works perfectly fine with ordinary HTML `<a>` elements. The router adds a listener that handles every click on a `<a>` element and tries to handle it on the client side, i.e., without doing another round trip to the server to request HTML. This is what enables the snappy “single-page app” navigations youre probably familiar with from most modern web apps.
The router will bail out of handling an `<a>` click under a number of situations
- the click event has had `prevent_default()` called on it
- the <kbd>Meta</kbd>, <kbd>Alt</kbd>, <kbd>Ctrl</kbd>, or <kbd>Shift</kbd> keys were held during click
- the `<a>` has a `target` or `download` attribute, or `rel="external"`
- the link has a different origin from the current location
In other words, the router will only try to do a client-side navigation when its pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., its a link to the page youre on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if its a link to the page youre currently on, you can match this attribute with a CSS selector.
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
<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>

View File

@@ -0,0 +1,23 @@
# Routing
## The Basics
Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?”
A URL consists of many parts. For example, the URL `https://leptos.dev/blog/search?q=Search#results` consists of
- a _scheme_: `https`
- a _domain_: `leptos.dev`
- a **path**: `/blog/search`
- a **query** (or **search**): `?q=Search`
- a _hash_: `#results`
The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page?
## The Philosophy
In most cases, the path should drive what is displayed on the page. From the 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 this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
The router handles most of this work for you by mapping the current location to particular components.

View File

@@ -15,11 +15,11 @@ For example, instead of embedding logic in a component directly like this:
```rust
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
// ⚠️ this is hard to test because it's embedded in the component
let maximum = move || todos.with(|todos| {
todos.iter().filter(|todo| todo.completed).sum()
});
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
// ⚠️ this is hard to test because it's embedded in the component
let num_remaining = move || todos.with(|todos| {
todos.iter().filter(|todo| !todo.completed).sum()
});
}
```
@@ -29,24 +29,24 @@ You could pull that logic out into a separate data structure and test it:
pub struct Todos(Vec<Todo>);
impl Todos {
pub fn remaining(&self) -> usize {
todos.iter().filter(|todo| todo.completed).sum()
}
pub fn num_remaining(&self) -> usize {
todos.iter().filter(|todo| !todo.completed).sum()
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_remaining {
// ...
}
#[test]
fn test_remaining {
// ...
}
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
// ✅ this has a test associated with it
let maximum = move || todos.with(Todos::remaining);
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
// ✅ this has a test associated with it
let num_remaining = move || todos.with(Todos::num_remaining);
}
```

View File

@@ -31,7 +31,7 @@ fn App(cx: Scope) -> impl IntoView {
set_count.update(|n| *n += 1);
}
>
"Click me"
"Click me: "
{move || count.get()}
</button>
}
@@ -61,7 +61,7 @@ Every component is a function with the following characteristics
## The Component Body
The body of the component function is a set-up function that runs once, not a
render function that re-runs multiple times. Youll typically use it to create 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.
@@ -110,7 +110,7 @@ than theyve ever used in their lives. And fair enough. Basically, passing a f
into the view tells the framework: “Hey, this is something that might change.”
When we click the button and call `set_count`, the `count` signal is updated. This
`move || count.get()` closure, whose value depends on the value of `count`, re-runs,
`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.

View File

@@ -30,7 +30,7 @@ Now lets say Id like to update the list of CSS classes on this element dyn
For example, lets say I want to add the class `red` when the count is odd. I can
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`)

View File

@@ -18,7 +18,7 @@ let double_count = move || count() * 2;
view! {
<progress
max="50"
value=progress
value=count
/>
<progress
max="50"
@@ -212,7 +212,7 @@ fn ProgressBar<F>(
progress: F
) -> impl IntoView
where
F: Fn() -> i32
F: Fn() -> i32 + 'static,
{
view! { cx,
<progress
@@ -227,18 +227,18 @@ This is a perfectly reasonable way to write this component: `progress` now takes
any value that implements this `Fn()` trait.
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
or as `progress: impl Fn() -> i32`, in part because theyre actually used to generate
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
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 proprs,
which allows you to pass props of different values easily.
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
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
is a enumerated type that represents any kind of readable reactive signal. It can
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.

View File

@@ -13,7 +13,7 @@ Leptos supports to two different patterns for iterating over items:
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 views, if you can render
any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render
`T`, you can render `Vec<T>`.
```rust
@@ -85,4 +85,4 @@ it is generated, and using that as an ID for the key function.
Check out the `<DynamicList/>` component below for an example.
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="100px"></iframe>
<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>

View File

@@ -54,7 +54,7 @@ event.
```rust
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
let input_element: NodeRef<HtmlElement<Input>> = NodeRef::new(cx);
let input_element: NodeRef<Input> = create_node_ref(cx);
```
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
underlying DOM node. Its value will be set when the element is rendered.

View File

@@ -26,7 +26,7 @@ things:
all of which can be rendered. Spending time in the `Option` and `Result` docs in particular
is one of the best ways to level up your Rust game.
4. And always remember: to be reactive, values must be functions. Youll see me constantly
wrap things in a `move ||` closure, below. This is to ensure that they actually re-run
wrap things in a `move ||` closure, below. This is to ensure that they actually rerun
when the signal they depend on changes, keeping the UI reactive.
## So What?
@@ -55,13 +55,13 @@ if its even. Well, how about this?
```rust
view! { cx,
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
}
```
@@ -74,15 +74,15 @@ Lets say we want to render some text if its odd, and nothing if its eve
```rust
let message = move || {
if is_odd() {
Some("Ding ding ding!")
} else {
None
}
if is_odd() {
Some("Ding ding ding!")
} else {
None
}
};
view! { cx,
<p>{message}</p>
<p>{message}</p>
}
```
@@ -91,7 +91,7 @@ This works fine. We can make it a little shorter if wed like, using `bool::th
```rust
let message = move || is_odd().then(|| "Ding ding ding!");
view! { cx,
<p>{message}</p>
<p>{message}</p>
}
```
@@ -105,15 +105,15 @@ pattern matching at your disposal.
```rust
let message = move || {
match value() {
0 => "Zero",
1 => "One",
n if is_odd() => "Odd",
_ => "Even"
}
match value() {
0 => "Zero",
1 => "One",
n if is_odd() => "Odd",
_ => "Even"
}
};
view! { cx,
<p>{message}</p>
<p>{message}</p>
}
```
@@ -134,13 +134,13 @@ But consider the following example:
let (value, set_value) = create_signal(cx, 0);
let message = move || if value() > 5 {
"Big"
"Big"
} else {
"Small"
"Small"
};
view! { cx,
<p>{message}</p>
<p>{message}</p>
}
```
@@ -148,11 +148,11 @@ This _works_, for sure. But if you added a log, you might be surprised
```rust
let message = move || if value() > 5 {
log!("{}: rendering Big", value());
"Big"
log!("{}: rendering Big", value());
"Big"
} else {
log!("{}: rendering Small", value());
"Small"
log!("{}: rendering Small", value());
"Small"
};
```
@@ -177,9 +177,9 @@ like this:
```rust
let message = move || if value() > 5 {
<Big/>
<Big/>
} else {
<Small/>
<Small/>
};
```
@@ -228,20 +228,20 @@ different branches of a conditional:
```rust,compile_error
view! { cx,
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }
}}
</main>
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }
}}
</main>
}
```
@@ -265,20 +265,20 @@ Heres the same example, with the conversion added:
```rust,compile_error
view! { cx,
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }.into_any()
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
}}
</main>
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }.into_any()
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
}}
</main>
}
```

View File

@@ -63,7 +63,7 @@ Lets add an `<ErrorBoundary/>` to this example.
fn NumericInput(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
<h1>"Error Handling"</h1>
@@ -77,9 +77,7 @@ let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
<p>"Not a number! Errors: "</p>
// we can render a list of errors as strings, if we'd like
<ul>
{move || errors.unwrap()
.get()
.0
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()

View File

@@ -30,11 +30,11 @@ it in the child. This lets you manipulate the state of the parent from the child
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<ButtonA setter=set_toggled/>
}
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<ButtonA setter=set_toggled/>
}
}
#[component]
@@ -63,11 +63,11 @@ Another approach would be to pass a callback to the child: say, `on_click`.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
}
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
}
}
@@ -106,13 +106,13 @@ in your `view` macro in `<App/>`.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
// note the on:click instead of on_click
// this is the same syntax as an HTML element event listener
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
}
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
// note the on:click instead of on_click
// this is the same syntax as an HTML element event listener
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
}
}
@@ -142,31 +142,32 @@ tree:
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout/>
}
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout/>
}
}
#[component]
pub fn Layout(cx: Scope) -> impl IntoView {
view! { cx,
<header>
<h1>"My Page"</h1>
<main>
<Content/>
</main>
}
view! { cx,
<header>
<h1>"My Page"</h1>
</header>
<main>
<Content/>
</main>
}
}
#[component]
pub fn Content(cx: Scope) -> impl IntoView {
view! { cx,
<div class="content">
<ButtonD/>
</div>
}
view! { cx,
<div class="content">
<ButtonD/>
</div>
}
}
#[component]
@@ -182,31 +183,32 @@ pass your `WriteSignal` to its props. You could do whats sometimes called
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout set_toggled/>
}
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout set_toggled/>
}
}
#[component]
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
view! { cx,
<header>
<h1>"My Page"</h1>
<main>
<Content set_toggled/>
</main>
}
view! { cx,
<header>
<h1>"My Page"</h1>
</header>
<main>
<Content set_toggled/>
</main>
}
}
#[component]
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
view! { cx,
<div class="content">
<ButtonD set_toggled/>
</div>
}
view! { cx,
<div class="content">
<ButtonD set_toggled/>
</div>
}
}
#[component]
@@ -236,26 +238,26 @@ unnecessary prop drilling.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
let (toggled, set_toggled) = create_signal(cx, false);
// share `set_toggled` with all children of this component
provide_context(cx, set_toggled);
// share `set_toggled` with all children of this component
provide_context(cx, set_toggled);
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout/>
}
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout/>
}
}
// <Layout/> and <Content/> omitted
#[component]
pub fn ButtonD(cx: Scope) -> impl IntoView {
// use_context searches up the context tree, hoping to
// find a `WriteSignal<bool>`
// in this case, I .expect() because I know I provided it
let setter = use_context::<WriteSignal<bool>>(cx)
.expect("to have found the setter provided");
// use_context searches up the context tree, hoping to
// find a `WriteSignal<bool>`
// in this case, I .expect() because I know I provided it
let setter = use_context::<WriteSignal<bool>>(cx)
.expect("to have found the setter provided");
view! { cx,
<button

View File

@@ -6,15 +6,15 @@ that enhances an HTML `<form>`. I need some way to pass all its inputs.
```rust
view! { cx,
<Form>
<fieldset>
<label>
"Some Input"
<input type="text" name="something"/>
</label>
</fieldset>
<button>"Submit"</button>
</Form>
<Form>
<fieldset>
<label>
"Some Input"
<input type="text" name="something"/>
</label>
</fieldset>
<button>"Submit"</button>
</Form>
}
```
@@ -30,13 +30,13 @@ In fact, youve already seen these both in action in the [`<Show/>`](/view/06_
```rust
view! { cx,
<Show
// `when` is a normal prop
// `when` is a normal prop
when=move || value() > 5
// `fallback` is a "render prop": a function that returns a view
// `fallback` is a "render prop": a function that returns a view
fallback=|cx| view! { cx, <Small/> }
>
// `<Big/>` (and anything else here)
// will be given to the `children` prop
// `<Big/>` (and anything else here)
// will be given to the `children` prop
<Big/>
</Show>
}
@@ -62,7 +62,7 @@ where
<h2>"Render Prop"</h2>
{render_prop()}
<h2>"Children"</h2>
<h2>"Children"</h2>
{children(cx)}
}
}
@@ -79,11 +79,11 @@ We can use the component like this:
```rust
view! { cx,
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
// these get passed to `children`
"Some text"
<span>"A span"</span>
</TakesChildren>
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
// these get passed to `children`
"Some text"
<span>"A span"</span>
</TakesChildren>
}
```
@@ -115,10 +115,12 @@ Calling it like this will create a list:
```rust
view! { cx,
<WrappedChildren>
"A"
"B"
"C"
</WrappedChildren>
<WrappedChildren>
"A"
"B"
"C"
</WrappedChildren>
}
```
<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>

View File

@@ -10,5 +10,7 @@ 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"

View File

@@ -1,9 +1,9 @@
use counter::*;
use leptos::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counter::*;
use leptos::*;
use web_sys::HtmlElement;
#[wasm_bindgen_test]
fn clear() {
@@ -84,22 +84,22 @@ fn inc() {
let clear = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.dyn_into::<web_sys::HtmlElement>()
.unwrap();
let dec = clear
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.dyn_into::<web_sys::HtmlElement>()
.unwrap();
let text = dec
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.dyn_into::<web_sys::HtmlElement>()
.unwrap();
let inc = text
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.dyn_into::<web_sys::HtmlElement>()
.unwrap();
inc.click();

View File

@@ -12,7 +12,6 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
broadcaster = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
serde = { version = "1", features = ["derive"] }
futures = "0.3"
cfg-if = "1"
lazy_static = "1"
@@ -23,9 +22,9 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "4.0.0"
gloo-net = { git = "https://github.com/rustwasm/gloo" }
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
[features]
default = []

View File

@@ -97,10 +97,10 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|_| get_server_count(),
);
let value = move || counter.read().map(|count| count.unwrap_or(0)).unwrap_or(0);
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter
.read()
.read(cx)
.map(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
@@ -143,7 +143,7 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
let value = move || {
log::debug!("FormCounter looking for value");
counter
.read()
.read(cx)
.map(|n| n.ok())
.flatten()
.map(|n| n)

View File

@@ -10,7 +10,6 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -1,9 +1,9 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
args = ["+stable", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
args = ["+stable", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,7 +2,6 @@ use counters::{Counters, CountersProps};
use leptos::*;
fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counters/> })

View File

@@ -1,7 +1,6 @@
use leptos::*;
fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counters/> })

View File

@@ -24,7 +24,6 @@ pub fn App(cx: Scope) -> impl IntoView {
// as strings, if we'd like
<ul>
{move || errors.get()
.0
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()

View File

@@ -7,31 +7,24 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
"serde",
] }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
log = "0.4.17"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
thiserror = "1.0.38"
tracing = "0.1.37"
wasm-bindgen = "0.2"
[features]
@@ -39,14 +32,14 @@ default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
@@ -54,12 +47,12 @@ denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "errors_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"

View File

@@ -13,7 +13,6 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -60,7 +60,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just implement our happy path and let the framework handle the rest
let cats_view = move || {
cats.with(|data| {
cats.with(cx, |data| {
data.iter()
.flatten()
.map(|cat| view! { cx, <img src={cat}/> })

View File

@@ -2,7 +2,6 @@ use fetch::fetch_example;
use leptos::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(fetch_example)

View File

@@ -7,12 +7,10 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1"
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
@@ -21,15 +19,13 @@ leptos_meta = { path = "../../meta", default-features = false }
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "4.0.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
tracing = "0.1"
# openssl = { version = "0.10", features = ["v110"] }
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
tracing = "0.1"
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]

View File

@@ -34,7 +34,7 @@ where
abort_controller.abort()
}
});
T::from_json(&json).ok()
T::de(&json).ok()
}
#[cfg(feature = "ssr")]
@@ -49,7 +49,7 @@ where
.text()
.await
.ok()?;
T::from_json(&json).map_err(|e| log::error!("{e}")).ok()
T::de(&json).map_err(|e| log::error!("{e}")).ok()
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]

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().unwrap_or(None).unwrap_or_default().len() < 28;
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@@ -82,7 +82,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
fallback=move || view! { cx, <p>"Loading..."</p> }
set_pending=set_pending.into()
>
{move || match stories.read() {
{move || match stories.read(cx) {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {

View File

@@ -17,13 +17,13 @@ pub fn Story(cx: Scope) -> impl IntoView {
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || story.read().map(|story| match story {
{move || story.read(cx).map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,
<div class="item-view">

View File

@@ -19,7 +19,7 @@ pub fn User(cx: Scope) -> impl IntoView {
view! { cx,
<div class="user-view">
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || user.read().map(|user| match user {
{move || user.read(cx).map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
Some(user) => view! { cx,
<div>

View File

@@ -7,13 +7,11 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
"serde",
] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
@@ -21,32 +19,31 @@ leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
tracing = "0.1"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
@@ -54,27 +51,27 @@ denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["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 = "hackernews_axum"
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
@@ -95,4 +92,4 @@ lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-default-features = false

View File

@@ -2,7 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="css" href="./static/style.css"/>
<link data-trunk rel="css" href="/style.css"/>
</head>
<body></body>
</html>

View File

@@ -34,7 +34,7 @@ where
abort_controller.abort()
}
});
T::from_json(&json).ok()
T::de(&json).ok()
}
#[cfg(feature = "ssr")]
@@ -49,7 +49,7 @@ where
.text()
.await
.ok()?;
T::from_json(&json).map_err(|e| log::error!("{e}")).ok()
T::de(&json).map_err(|e| log::error!("{e}")).ok()
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]

View File

@@ -1,4 +1,7 @@
use leptos::{view, Errors, For, ForProps, IntoView, RwSignal, Scope, View};
use leptos::{
signal_prelude::*, view, Errors, For, ForProps, IntoView, RwSignal, Scope,
View,
};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
@@ -6,21 +9,22 @@ pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {cx,
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=errors
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
view= move |cx, (_, error)| {
let error_string = error.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=errors
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
view= move |cx, (_, error)| {
let error_string = error.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
}
}
}
/>
}
.into_view(cx)

View File

@@ -46,7 +46,6 @@ if #[cfg(feature = "ssr")] {
use hackernews_axum::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {

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().unwrap_or(None).unwrap_or_default().len() < 28;
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@@ -82,7 +82,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
fallback=move || view! { cx, <p>"Loading..."</p> }
set_pending=set_pending.into()
>
{move || match stories.read() {
{move || match stories.read(cx) {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {

View File

@@ -17,13 +17,13 @@ pub fn Story(cx: Scope) -> impl IntoView {
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || story.read().map(|story| match story {
{move || story.read(cx).map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,
<div class="item-view">

View File

@@ -19,7 +19,7 @@ pub fn User(cx: Scope) -> impl IntoView {
view! { cx,
<div class="user-view">
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || user.read().map(|user| match user {
{move || user.read(cx).map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
Some(user) => view! { cx,
<div>

View File

@@ -0,0 +1,7 @@
[workspace]
members = ["client", "api-boundary", "server"]
[patch.crates-io]
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router" }
api-boundary = { path = "api-boundary" }

View File

@@ -0,0 +1,9 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -0,0 +1,23 @@
# Leptos Login Example
This example demonstrates a scenario of a client-side rendered application
that uses an existing API that you cannot or do not want to change.
The authentications of this example are done using an API token.
## Run
First start the example server:
```
cd server/ && cargo run
```
then use [`trunk`](https://trunkrs.dev) to serve the SPA:
```
cd client/ && trunk serve
```
finally you can visit the web application at `http://localhost:8080`
The `api-boundary` crate contains data structures that are used by the server and the client.

View File

@@ -0,0 +1,8 @@
[package]
name = "api-boundary"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
serde = { version = "1.0", features = ["derive"] }

View File

@@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Credentials {
pub email: String,
pub password: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct UserInfo {
pub email: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ApiToken {
pub token: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Error {
pub message: String,
}

View File

@@ -0,0 +1,19 @@
[package]
name = "client"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
api-boundary = "*"
leptos = { version = "0.2.0-alpha2", features = ["stable"] }
leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
log = "0.4"
console_error_panic_hook = "0.1"
console_log = "0.2"
gloo-net = "0.2"
gloo-storage = "0.2"
serde = "1.0"
thiserror = "1.0"

View File

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

View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,94 @@
use gloo_net::http::{Request, Response};
use serde::de::DeserializeOwned;
use thiserror::Error;
use api_boundary::*;
#[derive(Clone, Copy)]
pub struct UnauthorizedApi {
url: &'static str,
}
#[derive(Clone)]
pub struct AuthorizedApi {
url: &'static str,
token: ApiToken,
}
impl UnauthorizedApi {
pub const fn new(url: &'static str) -> Self {
Self { url }
}
pub async fn register(&self, credentials: &Credentials) -> Result<()> {
let url = format!("{}/users", self.url);
let response = Request::post(&url).json(credentials)?.send().await?;
into_json(response).await
}
pub async fn login(
&self,
credentials: &Credentials,
) -> Result<AuthorizedApi> {
let url = format!("{}/login", self.url);
let response = Request::post(&url).json(credentials)?.send().await?;
let token = into_json(response).await?;
Ok(AuthorizedApi::new(self.url, token))
}
}
impl AuthorizedApi {
pub const fn new(url: &'static str, token: ApiToken) -> Self {
Self { url, token }
}
fn auth_header_value(&self) -> String {
format!("Bearer {}", self.token.token)
}
async fn send<T>(&self, req: Request) -> Result<T>
where
T: DeserializeOwned,
{
let response = req
.header("Authorization", &self.auth_header_value())
.send()
.await?;
into_json(response).await
}
pub async fn logout(&self) -> Result<()> {
let url = format!("{}/logout", self.url);
self.send(Request::post(&url)).await
}
pub async fn user_info(&self) -> Result<UserInfo> {
let url = format!("{}/users", self.url);
self.send(Request::get(&url)).await
}
pub fn token(&self) -> &ApiToken {
&self.token
}
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Fetch(#[from] gloo_net::Error),
#[error("{0:?}")]
Api(api_boundary::Error),
}
impl From<api_boundary::Error> for Error {
fn from(e: api_boundary::Error) -> Self {
Self::Api(e)
}
}
async fn into_json<T>(response: Response) -> Result<T>
where
T: DeserializeOwned,
{
// ensure we've got 2xx status
if response.ok() {
Ok(response.json().await?)
} else {
Err(response.json::<api_boundary::Error>().await?.into())
}
}

View File

@@ -0,0 +1,73 @@
use leptos::{ev, *};
#[component]
pub fn CredentialsForm(
cx: Scope,
title: &'static str,
action_label: &'static str,
action: Action<(String, String), ()>,
error: Signal<Option<String>>,
disabled: Signal<bool>,
) -> impl IntoView {
let (password, set_password) = create_signal(cx, String::new());
let (email, set_email) = create_signal(cx, String::new());
let dispatch_action =
move || action.dispatch((email.get(), password.get()));
let button_is_disabled = Signal::derive(cx, move || {
disabled.get() || password.get().is_empty() || email.get().is_empty()
});
view! { cx,
<form on:submit=|ev|ev.prevent_default()>
<p>{ title }</p>
{move || error.get().map(|err| view!{ cx,
<p style ="color:red;" >{ err }</p>
})}
<input
type = "email"
required
placeholder = "Email address"
prop:disabled = move || disabled.get()
on:keyup = move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_email.update(|v|*v = val);
}
// The `change` event fires when the browser fills the form automatically,
on:change = move |ev| {
let val = event_target_value(&ev);
set_email.update(|v|*v = val);
}
/>
<input
type = "password"
required
placeholder = "Password"
prop:disabled = move || disabled.get()
on:keyup = move |ev: ev::KeyboardEvent| {
match &*ev.key() {
"Enter" => {
dispatch_action();
}
_=> {
let val = event_target_value(&ev);
set_password.update(|p|*p = val);
}
}
}
// The `change` event fires when the browser fills the form automatically,
on:change = move |ev| {
let val = event_target_value(&ev);
set_password.update(|p|*p = val);
}
/>
<button
prop:disabled = move || button_is_disabled.get()
on:click = move |_| dispatch_action()
>
{ action_label }
</button>
</form>
}
}

View File

@@ -0,0 +1,4 @@
pub mod credentials;
pub mod navbar;
pub use self::{credentials::*, navbar::*};

View File

@@ -0,0 +1,32 @@
use leptos::*;
use leptos_router::*;
use crate::Page;
#[component]
pub fn NavBar<F>(
cx: Scope,
logged_in: Signal<bool>,
on_logout: F,
) -> impl IntoView
where
F: Fn() + 'static + Clone,
{
view! { cx,
<nav>
<Show
when = move || logged_in.get()
fallback = |cx| view! { cx,
<A href=Page::Login.path() >"Login"</A>
" | "
<A href=Page::Register.path() >"Register"</A>
}
>
<a href="#" on:click={
let on_logout = on_logout.clone();
move |_| on_logout()
}>"Logout"</a>
</Show>
</nav>
}
}

View File

@@ -0,0 +1,130 @@
use gloo_storage::{LocalStorage, Storage};
use leptos::*;
use leptos_router::*;
use api_boundary::*;
mod api;
mod components;
mod pages;
use self::{components::*, pages::*};
const DEFAULT_API_URL: &str = "/api";
const API_TOKEN_STORAGE_KEY: &str = "api-token";
#[component]
pub fn App(cx: Scope) -> impl IntoView {
// -- signals -- //
let authorized_api = create_rw_signal(cx, None::<api::AuthorizedApi>);
let user_info = create_rw_signal(cx, None::<UserInfo>);
let logged_in = Signal::derive(cx, move || authorized_api.get().is_some());
// -- actions -- //
let fetch_user_info = create_action(cx, move |_| async move {
match authorized_api.get() {
Some(api) => match api.user_info().await {
Ok(info) => {
user_info.update(|i| *i = Some(info));
}
Err(err) => {
log::error!("Unable to fetch user info: {err}")
}
},
None => {
log::error!("Unable to fetch user info: not logged in")
}
}
});
let logout = create_action(cx, move |_| async move {
match authorized_api.get() {
Some(api) => match api.logout().await {
Ok(_) => {
authorized_api.update(|a| *a = None);
user_info.update(|i| *i = None);
}
Err(err) => {
log::error!("Unable to logout: {err}")
}
},
None => {
log::error!("Unable to logout user: not logged in")
}
}
});
// -- callbacks -- //
let on_logout = move || {
logout.dispatch(());
};
// -- init API -- //
let unauthorized_api = api::UnauthorizedApi::new(DEFAULT_API_URL);
if let Ok(token) = LocalStorage::get(API_TOKEN_STORAGE_KEY) {
let api = api::AuthorizedApi::new(DEFAULT_API_URL, token);
authorized_api.update(|a| *a = Some(api));
fetch_user_info.dispatch(());
}
log::debug!("User is logged in: {}", logged_in.get());
// -- effects -- //
create_effect(cx, move |_| {
log::debug!("API authorization state changed");
match authorized_api.get() {
Some(api) => {
log::debug!(
"API is now authorized: save token in LocalStorage"
);
LocalStorage::set(API_TOKEN_STORAGE_KEY, api.token())
.expect("LocalStorage::set");
}
None => {
log::debug!("API is no longer authorized: delete token from LocalStorage");
LocalStorage::delete(API_TOKEN_STORAGE_KEY);
}
}
});
view! { cx,
<Router>
<NavBar logged_in on_logout />
<main>
<Routes>
<Route
path=Page::Home.path()
view=move |cx| view! { cx,
<Home user_info = user_info.into() />
}
/>
<Route
path=Page::Login.path()
view=move |cx| view! { cx,
<Login
api = unauthorized_api
on_success = move |api| {
log::info!("Successfully logged in");
authorized_api.update(|v| *v = Some(api));
let navigate = use_navigate(cx);
navigate(Page::Home.path(), Default::default()).expect("Home route");
fetch_user_info.dispatch(());
} />
}
/>
<Route
path=Page::Register.path()
view=move |cx| view! { cx,
<Register api = unauthorized_api />
}
/>
</Routes>
</main>
</Router>
}
}

View File

@@ -0,0 +1,9 @@
use leptos::*;
use client::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <App /> })
}

View File

@@ -0,0 +1,20 @@
use crate::Page;
use api_boundary::UserInfo;
use leptos::*;
use leptos_router::*;
#[component]
pub fn Home(cx: Scope, user_info: Signal<Option<UserInfo>>) -> impl IntoView {
view! { cx,
<h2>"Leptos Login example"</h2>
{move || match user_info.get() {
Some(info) => view!{ cx,
<p>"You are logged in with "{ info.email }"."</p>
}.into_view(cx),
None => view!{ cx,
<p>"You are not logged in."</p>
<A href=Page::Login.path() >"Login now."</A>
}.into_view(cx)
}}
}
}

View File

@@ -0,0 +1,66 @@
use leptos::*;
use leptos_router::*;
use api_boundary::*;
use crate::{
api::{self, AuthorizedApi, UnauthorizedApi},
components::credentials::*,
Page,
};
#[component]
pub fn Login<F>(cx: Scope, api: UnauthorizedApi, on_success: F) -> impl IntoView
where
F: Fn(AuthorizedApi) + 'static + Clone,
{
let (login_error, set_login_error) = create_signal(cx, None::<String>);
let (wait_for_response, set_wait_for_response) = create_signal(cx, false);
let login_action =
create_action(cx, move |(email, password): &(String, String)| {
log::debug!("Try to login with {email}");
let email = email.to_string();
let password = password.to_string();
let credentials = Credentials { email, password };
let on_success = on_success.clone();
async move {
set_wait_for_response.update(|w| *w = true);
let result = api.login(&credentials).await;
set_wait_for_response.update(|w| *w = false);
match result {
Ok(res) => {
set_login_error.update(|e| *e = None);
on_success(res);
}
Err(err) => {
let msg = match err {
api::Error::Fetch(js_err) => {
format!("{js_err:?}")
}
api::Error::Api(err) => err.message,
};
error!(
"Unable to login with {}: {msg}",
credentials.email
);
set_login_error.update(|e| *e = Some(msg));
}
}
}
});
let disabled = Signal::derive(cx, move || wait_for_response.get());
view! { cx,
<CredentialsForm
title = "Please login to your account"
action_label = "Login"
action = login_action
error = login_error.into()
disabled
/>
<p>"Don't have an account?"</p>
<A href=Page::Register.path()>"Register"</A>
}
}

View File

@@ -0,0 +1,23 @@
pub mod home;
pub mod login;
pub mod register;
pub use self::{home::*, login::*, register::*};
#[derive(Debug, Clone, Copy, Default)]
pub enum Page {
#[default]
Home,
Login,
Register,
}
impl Page {
pub fn path(&self) -> &'static str {
match self {
Self::Home => "/",
Self::Login => "/login",
Self::Register => "/register",
}
}
}

View File

@@ -0,0 +1,77 @@
use leptos::*;
use leptos_router::*;
use api_boundary::*;
use crate::{
api::{self, UnauthorizedApi},
components::credentials::*,
Page,
};
#[component]
pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
let (register_response, set_register_response) =
create_signal(cx, None::<()>);
let (register_error, set_register_error) =
create_signal(cx, None::<String>);
let (wait_for_response, set_wait_for_response) = create_signal(cx, false);
let register_action =
create_action(cx, move |(email, password): &(String, String)| {
let email = email.to_string();
let password = password.to_string();
let credentials = Credentials { email, password };
log!("Try to register new account for {}", credentials.email);
async move {
set_wait_for_response.update(|w| *w = true);
let result = api.register(&credentials).await;
set_wait_for_response.update(|w| *w = false);
match result {
Ok(res) => {
set_register_response.update(|v| *v = Some(res));
set_register_error.update(|e| *e = None);
}
Err(err) => {
let msg = match err {
api::Error::Fetch(js_err) => {
format!("{js_err:?}")
}
api::Error::Api(err) => err.message,
};
log::warn!(
"Unable to register new account for {}: {msg}",
credentials.email
);
set_register_error.update(|e| *e = Some(msg));
}
}
}
});
let disabled = Signal::derive(cx, move || wait_for_response.get());
view! { cx,
<Show
when = move || register_response.get().is_some()
fallback = move |_| view!{ cx,
<CredentialsForm
title = "Please enter the desired credentials"
action_label = "Register"
action = register_action
error = register_error.into()
disabled
/>
<p>"Your already have an account?"</p>
<A href=Page::Login.path()>"Login"</A>
}
>
<p>"You have successfully registered."</p>
<p>
"You can now "
<A href=Page::Login.path()>"login"</A>
" with your new account."
</p>
</Show>
}
}

View File

@@ -0,0 +1,18 @@
[package]
name = "server"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
anyhow = "1.0"
api-boundary = "*"
axum = { version = "0.6", features = ["headers"] }
env_logger = "0.10"
log = "0.4"
mailparse = "0.14"
pwhash = "1.0"
thiserror = "1.0"
tokio = { version = "1.25", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.4", features = ["cors"] }
uuid = { version = "1.3", features = ["v4"] }

View File

@@ -0,0 +1,114 @@
use crate::{application::*, Error};
use api_boundary as json;
use axum::{
http::StatusCode,
response::Json,
response::{IntoResponse, Response},
};
use thiserror::Error;
impl From<InvalidEmailAddress> for json::Error {
fn from(_: InvalidEmailAddress) -> Self {
Self {
message: "Invalid email address".to_string(),
}
}
}
impl From<InvalidPassword> for json::Error {
fn from(err: InvalidPassword) -> Self {
let InvalidPassword::TooShort(min_len) = err;
Self {
message: format!("Invalid password (min. length = {min_len})"),
}
}
}
impl From<CreateUserError> for json::Error {
fn from(err: CreateUserError) -> Self {
let message = match err {
CreateUserError::UserExists => "User already exits".to_string(),
};
Self { message }
}
}
impl From<LoginError> for json::Error {
fn from(err: LoginError) -> Self {
let message = match err {
LoginError::InvalidEmailOrPassword => {
"Invalid email or password".to_string()
}
};
Self { message }
}
}
impl From<LogoutError> for json::Error {
fn from(err: LogoutError) -> Self {
let message = match err {
LogoutError::NotLoggedIn => "No user is logged in".to_string(),
};
Self { message }
}
}
impl From<AuthError> for json::Error {
fn from(err: AuthError) -> Self {
let message = match err {
AuthError::NotAuthorized => "Not authorized".to_string(),
};
Self { message }
}
}
impl From<CredentialParsingError> for json::Error {
fn from(err: CredentialParsingError) -> Self {
match err {
CredentialParsingError::EmailAddress(err) => err.into(),
CredentialParsingError::Password(err) => err.into(),
}
}
}
#[derive(Debug, Error)]
pub enum CredentialParsingError {
#[error(transparent)]
EmailAddress(#[from] InvalidEmailAddress),
#[error(transparent)]
Password(#[from] InvalidPassword),
}
impl TryFrom<json::Credentials> for Credentials {
type Error = CredentialParsingError;
fn try_from(
json::Credentials { email, password }: json::Credentials,
) -> Result<Self, Self::Error> {
let email: EmailAddress = email.parse()?;
let password = Password::try_from(password)?;
Ok(Self { email, password })
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let (code, value) = match self {
Self::Logout(err) => {
(StatusCode::BAD_REQUEST, json::Error::from(err))
}
Self::Login(err) => {
(StatusCode::BAD_REQUEST, json::Error::from(err))
}
Self::Credentials(err) => {
(StatusCode::BAD_REQUEST, json::Error::from(err))
}
Self::CreateUser(err) => {
(StatusCode::BAD_REQUEST, json::Error::from(err))
}
Self::Auth(err) => {
(StatusCode::UNAUTHORIZED, json::Error::from(err))
}
};
(code, Json(value)).into_response()
}
}

View File

@@ -0,0 +1,159 @@
use mailparse::addrparse;
use pwhash::bcrypt;
use std::{collections::HashMap, str::FromStr, sync::RwLock};
use thiserror::Error;
use uuid::Uuid;
#[derive(Default)]
pub struct AppState {
users: RwLock<HashMap<EmailAddress, Password>>,
tokens: RwLock<HashMap<Uuid, EmailAddress>>,
}
impl AppState {
pub fn create_user(
&self,
credentials: Credentials,
) -> Result<(), CreateUserError> {
let Credentials { email, password } = credentials;
let user_exists = self.users.read().unwrap().get(&email).is_some();
if user_exists {
return Err(CreateUserError::UserExists);
}
self.users.write().unwrap().insert(email, password);
Ok(())
}
pub fn login(
&self,
email: EmailAddress,
password: &str,
) -> Result<Uuid, LoginError> {
let valid_credentials = self
.users
.read()
.unwrap()
.get(&email)
.map(|hashed_password| hashed_password.verify(password))
.unwrap_or(false);
if !valid_credentials {
Err(LoginError::InvalidEmailOrPassword)
} else {
let token = Uuid::new_v4();
self.tokens.write().unwrap().insert(token, email);
Ok(token)
}
}
pub fn logout(&self, token: &str) -> Result<(), LogoutError> {
let token = token
.parse::<Uuid>()
.map_err(|_| LogoutError::NotLoggedIn)?;
self.tokens.write().unwrap().remove(&token);
Ok(())
}
pub fn authorize_user(
&self,
token: &str,
) -> Result<CurrentUser, AuthError> {
token
.parse::<Uuid>()
.map_err(|_| AuthError::NotAuthorized)
.and_then(|token| {
self.tokens
.read()
.unwrap()
.get(&token)
.cloned()
.map(|email| CurrentUser { email, token })
.ok_or(AuthError::NotAuthorized)
})
}
}
#[derive(Debug, Error)]
pub enum CreateUserError {
#[error("The user already exists")]
UserExists,
}
#[derive(Debug, Error)]
pub enum LoginError {
#[error("Invalid email or password")]
InvalidEmailOrPassword,
}
#[derive(Debug, Error)]
pub enum LogoutError {
#[error("You are not logged in")]
NotLoggedIn,
}
#[derive(Debug, Error)]
pub enum AuthError {
#[error("You are not authorized")]
NotAuthorized,
}
pub struct Credentials {
pub email: EmailAddress,
pub password: Password,
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct EmailAddress(String);
#[derive(Debug, Error)]
#[error("The given email address is invalid")]
pub struct InvalidEmailAddress;
impl FromStr for EmailAddress {
type Err = InvalidEmailAddress;
fn from_str(s: &str) -> Result<Self, Self::Err> {
addrparse(s)
.ok()
.and_then(|parsed| parsed.extract_single_info())
.map(|single_info| Self(single_info.addr))
.ok_or(InvalidEmailAddress)
}
}
impl EmailAddress {
pub fn into_string(self) -> String {
self.0
}
}
#[derive(Clone)]
pub struct CurrentUser {
pub email: EmailAddress,
pub token: Uuid,
}
const MIN_PASSWORD_LEN: usize = 3;
pub struct Password(String);
impl Password {
pub fn verify(&self, password: &str) -> bool {
bcrypt::verify(password, &self.0)
}
}
#[derive(Debug, Error)]
pub enum InvalidPassword {
#[error("Password is too short (min. length is {0})")]
TooShort(usize),
}
impl TryFrom<String> for Password {
type Error = InvalidPassword;
fn try_from(p: String) -> Result<Self, Self::Error> {
if p.len() < MIN_PASSWORD_LEN {
return Err(InvalidPassword::TooShort(MIN_PASSWORD_LEN));
}
let hashed = bcrypt::hash(&p).unwrap();
Ok(Self(hashed))
}
}

View File

@@ -0,0 +1,113 @@
use std::{env, sync::Arc};
use axum::{
extract::{State, TypedHeader},
headers::{authorization::Bearer, Authorization},
http::Method,
response::Json,
routing::{get, post},
Router,
};
use tower_http::cors::{Any, CorsLayer};
use api_boundary as json;
mod adapters;
mod application;
use self::application::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
if let Err(err) = env::var("RUST_LOG") {
match err {
env::VarError::NotPresent => {
env::set_var("RUST_LOG", "debug");
}
env::VarError::NotUnicode(_) => {
return Err(anyhow::anyhow!("The value of 'RUST_LOG' does not contain valid unicode data."));
}
}
}
env_logger::init();
let shared_state = Arc::new(AppState::default());
let cors_layer = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
.allow_origin(Any);
let app = Router::new()
.route("/login", post(login))
.route("/logout", post(logout))
.route("/users", post(create_user))
.route("/users", get(get_user_info))
.route_layer(cors_layer)
.with_state(shared_state);
let addr = "0.0.0.0:3000".parse().unwrap();
log::info!("Listen on {addr}");
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}
type Result<T> = std::result::Result<Json<T>, Error>;
/// API error
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
enum Error {
#[error(transparent)]
CreateUser(#[from] CreateUserError),
#[error(transparent)]
Login(#[from] LoginError),
#[error(transparent)]
Logout(#[from] LogoutError),
#[error(transparent)]
Auth(#[from] AuthError),
#[error(transparent)]
Credentials(#[from] adapters::CredentialParsingError),
}
async fn create_user(
State(state): State<Arc<AppState>>,
Json(credentials): Json<json::Credentials>,
) -> Result<()> {
let credentials = Credentials::try_from(credentials)?;
state.create_user(credentials)?;
Ok(Json(()))
}
async fn login(
State(state): State<Arc<AppState>>,
Json(credentials): Json<json::Credentials>,
) -> Result<json::ApiToken> {
let json::Credentials { email, password } = credentials;
log::debug!("{email} tries to login");
let email = email.parse().map_err(|_|
// Here we don't want to leak detailed info.
LoginError::InvalidEmailOrPassword)?;
let token = state.login(email, &password).map(|s| s.to_string())?;
Ok(Json(json::ApiToken { token }))
}
async fn logout(
State(state): State<Arc<AppState>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<()> {
state.logout(auth.token())?;
Ok(Json(()))
}
async fn get_user_info(
State(state): State<Arc<AppState>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<json::UserInfo> {
let user = state.authorize_user(auth.token())?;
let CurrentUser { email, .. } = user;
Ok(Json(json::UserInfo {
email: email.into_string(),
}))
}

View File

@@ -7,7 +7,8 @@ use web_sys::MouseEvent;
// for the child component to write into and the parent to read
// 2) <ButtonB/>: passing a closure as one of the child component props, for
// the child component to call
// 4) <ButtonC/>: providing a context that is used in the component (rather than prop drilling)
// 3) <ButtonC/>: adding an `on:` event listener to a component
// 4) <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
#[derive(Copy, Clone)]
struct SmallcapsContext(WriteSignal<bool>);
@@ -17,6 +18,7 @@ pub fn App(cx: Scope) -> impl IntoView {
// just some signals to toggle three classes on our <p>
let (red, set_red) = create_signal(cx, false);
let (right, set_right) = create_signal(cx, false);
let (italics, set_italics) = create_signal(cx, false);
let (smallcaps, set_smallcaps) = create_signal(cx, false);
// the newtype pattern isn't *necessary* here but is a good practice
@@ -31,6 +33,7 @@ pub fn App(cx: Scope) -> impl IntoView {
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
class:red=red
class:right=right
class:italics=italics
class:smallcaps=smallcaps
>
"Lorem ipsum sit dolor amet."
@@ -42,8 +45,13 @@ pub fn App(cx: Scope) -> impl IntoView {
// Button B: pass a closure
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
// Button B: use a regular event listener
// setting an event listener on a component like this applies it
// to each of the top-level elements the component returns
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
// Button D gets its setter from context rather than props
<ButtonC/>
<ButtonD/>
</main>
}
}
@@ -53,7 +61,7 @@ pub fn App(cx: Scope) -> impl IntoView {
pub fn ButtonA(
cx: Scope,
/// Signal that will be toggled when the button is clicked.
setter: WriteSignal<bool>
setter: WriteSignal<bool>,
) -> impl IntoView {
view! {
cx,
@@ -70,7 +78,7 @@ pub fn ButtonA(
pub fn ButtonB<F>(
cx: Scope,
/// Callback that will be invoked when the button is clicked.
on_click: F
on_click: F,
) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
@@ -97,10 +105,22 @@ where
// if Rust ever had named function arguments we could drop this requirement
}
/// Button C is a dummy: it renders a button but doesn't handle
/// its click. Instead, the parent component adds an event listener.
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
view! {
cx,
<button>
"Toggle Italics"
</button>
}
}
/// Button D is very similar to Button A, but instead of passing the setter as a prop
/// we get it from the context
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
pub fn ButtonD(cx: Scope) -> impl IntoView {
let setter = use_context::<SmallcapsContext>(cx).unwrap().0;
view! {

View File

@@ -71,9 +71,10 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
});
let location = use_location(cx);
let contacts = create_resource(cx, move || location.search.get(), get_contacts);
let contacts =
create_resource(cx, move || location.search.get(), get_contacts);
let contacts = move || {
contacts.read().map(|contacts| {
contacts.read(cx).map(|contacts| {
// this data doesn't change frequently so we can use .map().collect() instead of a keyed <For/>
contacts
.into_iter()
@@ -126,12 +127,19 @@ pub fn Contact(cx: Scope) -> impl IntoView {
get_contact,
);
let contact_display = move || match contact.read() {
create_effect(cx, move |_| {
log!("params = {:#?}", params.get());
});
let contact_display = move || match contact.read(cx) {
// None => loading, but will be caught by Suspense fallback
// I'm only doing this explicitly for the example
None => None,
// Some(None) => has loaded and found no contact
Some(None) => Some(view! { cx, <p>"No contact with this ID was found."</p> }.into_any()),
Some(None) => Some(
view! { cx, <p>"No contact with this ID was found."</p> }
.into_any(),
),
// Some(Some) => has loaded and found a contact
Some(Some(contact)) => Some(
view! { cx,

View File

@@ -2,7 +2,6 @@ use leptos::*;
use router::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <RouterExample/> })

View File

@@ -0,0 +1 @@
use flake

View File

@@ -0,0 +1,112 @@
[package]
name = "session_auth_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../meta", default-features = false }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
axum_sessions_auth = { version = "7.0.0", features = [
"sqlite-rustls",
], optional = true }
axum_database_sessions = { version = "7.0.0", features = [
"sqlite-rustls",
], optional = true }
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",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:axum_sessions_auth",
"dep:axum_database_sessions",
"dep:async-trait",
"dep:sqlx",
"dep:bcrypt",
"dep:rand",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["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 = "session_auth_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,9 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -0,0 +1,42 @@
# Leptos Authenticated Todo App Sqlite with Axum
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server. It lets you login, signup, and submit todos as different users, or a guest.
## Client Side Rendering
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
```
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
```bash
cargo run --no-default-features --features=ssr
```

Binary file not shown.

80
examples/session_auth_axum/flake.lock generated Normal file
View File

@@ -0,0 +1,80 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1672580127,
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0874168639713f547c05947c76124f78441ea46c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1677292251,
"narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -0,0 +1,37 @@
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
inputs.rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
in
with pkgs; rec {
devShells.default = mkShell {
shellHook = ''
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig";
'';
nativeBuildInputs = [
pkg-config
];
buildInputs = [
trunk
sqlite
sass
openssl
(rust-bin.nightly.latest.default.override {
extensions = [ "rust-src" ];
targets = [ "wasm32-unknown-unknown" ];
})
];
};
});
}

View File

@@ -0,0 +1,27 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS user_permissions (
user_id INTEGER NOT NULL,
token TEXT NOT NULL
);
-- INSERT INTO users (id, anonymous, username, password)
-- SELECT 0, true, 'Guest', ''
-- ON CONFLICT(id) DO UPDATE SET
-- anonymous = EXCLUDED.anonymous,
-- username = EXCLUDED.username;
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
completed BOOLEAN,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,231 @@
use std::collections::HashSet;
use cfg_if::cfg_if;
use leptos::*;
use serde::{Deserialize, Serialize};
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::SqlitePool;
use axum_sessions_auth::{SessionSqlitePool, Authentication, HasPermission};
use bcrypt::{hash, verify, DEFAULT_COST};
use crate::todo::{pool, auth};
pub type AuthSession = axum_sessions_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
}}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct User {
pub id: i64,
pub username: String,
pub password: String,
pub permissions: HashSet<String>,
}
impl Default for User {
fn default() -> Self {
let permissions = HashSet::new();
Self {
id: -1,
username: "Guest".into(),
password: "".into(),
permissions,
}
}
}
cfg_if! {
if #[cfg(feature = "ssr")] {
use async_trait::async_trait;
impl User {
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
.bind(id)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = ?;",
)
.bind(id)
.fetch_all(pool)
.await
.ok()?;
Some(sqluser.into_user(Some(sql_user_perms)))
}
pub async fn get_from_username(name: String, pool: &SqlitePool) -> Option<Self> {
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE username = ?")
.bind(name)
.fetch_one(pool)
.await
.ok()?;
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
"SELECT token FROM user_permissions WHERE user_id = ?;",
)
.bind(sqluser.id)
.fetch_all(pool)
.await
.ok()?;
Some(sqluser.into_user(Some(sql_user_perms)))
}
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlPermissionTokens {
pub token: String,
}
#[async_trait]
impl Authentication<User, i64, SqlitePool> for User {
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
let pool = pool.unwrap();
User::get(userid, pool)
.await
.ok_or_else(|| anyhow::anyhow!("Cannot get user"))
}
fn is_authenticated(&self) -> bool {
true
}
fn is_active(&self) -> bool {
true
}
fn is_anonymous(&self) -> bool {
false
}
}
#[async_trait]
impl HasPermission<SqlitePool> for User {
async fn has(&self, perm: &str, _pool: &Option<&SqlitePool>) -> bool {
self.permissions.contains(perm)
}
}
#[derive(sqlx::FromRow, Clone)]
pub struct SqlUser {
pub id: i64,
pub username: String,
pub password: String,
}
impl SqlUser {
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
User {
id: self.id,
username: self.username,
password: self.password,
permissions: if let Some(user_perms) = sql_user_perms {
user_perms
.into_iter()
.map(|x| x.token)
.collect::<HashSet<String>>()
} else {
HashSet::<String>::new()
},
}
}
}
}
}
#[server(Foo, "/api")]
pub async fn foo() -> Result<String, ServerFnError> {
Ok(String::from("Bar!"))
}
#[server(GetUser, "/api")]
pub async fn get_user(cx: Scope) -> Result<Option<User>, ServerFnError> {
let auth = auth(cx)?;
Ok(auth.current_user)
}
#[server(Login, "/api")]
pub async fn login(
cx: Scope,
username: String,
password: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
let pool = pool(cx)?;
let auth = auth(cx)?;
let user: User = User::get_from_username(username, &pool)
.await
.ok_or("User does not exist.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
match verify(password, &user.password).map_err(|e| ServerFnError::ServerError(e.to_string()))? {
true => {
auth.login_user(user.id);
auth.remember_user(remember.is_some());
leptos_axum::redirect(cx, "/");
Ok(())
}
false => Err(ServerFnError::ServerError(
"Password does not match.".to_string(),
)),
}
}
#[server(Signup, "/api")]
pub async fn signup(
cx: Scope,
username: String,
password: String,
password_confirmation: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
let pool = pool(cx)?;
let auth = auth(cx)?;
if password != password_confirmation {
return Err(ServerFnError::ServerError(
"Passwords did not match.".to_string(),
));
}
let password_hashed = hash(password, DEFAULT_COST).unwrap();
sqlx::query("INSERT INTO users (username, password) VALUES (?,?)")
.bind(username.clone())
.bind(password_hashed)
.execute(&pool)
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
let user = User::get_from_username(username, &pool)
.await
.ok_or("Signup failed: User does not exist.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
auth.login_user(user.id);
auth.remember_user(remember.is_some());
leptos_axum::redirect(cx, "/");
Ok(())
}
#[server(Logout, "/api")]
pub async fn logout(cx: Scope) -> Result<(), ServerFnError> {
let auth = auth(cx)?;
auth.logout_user();
leptos_axum::redirect(cx, "/");
Ok(())
}

View File

@@ -0,0 +1,61 @@
use crate::errors::TodoAppError;
use cfg_if::cfg_if;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
#[component]
pub fn ErrorTemplate(
cx: Scope,
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(cx, e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<TodoAppError> = errors
.get()
.into_iter()
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
.collect();
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! {
if #[cfg(feature="ssr")]{
let response = use_context::<ResponseOptions>(cx);
if let Some(response) = response{
response.set_status(errors[0].status_code());
}
}
}
view! {cx,
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
view= move |cx, error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
cx,
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

View File

@@ -0,0 +1,19 @@
use http::status::StatusCode;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum TodoAppError {
#[error("Not Found")]
NotFound,
#[error("Internal Server Error")]
InternalServerError,
}
impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

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