Compare commits

...

262 Commits
3014 ... clippy

Author SHA1 Message Date
Greg Johnston
047f0dda0e chore: clear clippy warnings 2023-03-21 17:12:35 -04:00
Greg Johnston
c086ce21a6 v0.2.4 2023-03-21 17:12:35 -04:00
martin frances
d371b09eda clippy: simplify Box::pin() call. (#718) 2023-03-21 17:12:35 -04:00
Greg Johnston
5a1d249626 fix: <Transition/> behavior (#717) 2023-03-21 17:12:34 -04:00
Greg Johnston
aa0fa40eac docs: small fixes (#715) 2023-03-21 17:12:34 -04:00
Carlton Gibson
e36a2ba6df 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-21 17:12:34 -04:00
Greg Johnston
2072d69cad chore: clear warning and add exports of helpers with handles 2023-03-21 17:12:34 -04:00
Greg Johnston
2c5253c2ce chore: handle unbounded_send warnings 2023-03-21 17:12:34 -04:00
Greg Johnston
3558439577 feat: add Scope::batch() (#711) 2023-03-21 17:12:34 -04:00
Greg Johnston
b82a15cf67 feat: allow manual signal disposal before the scope is disposed (#710) 2023-03-21 17:12:34 -04:00
Greg Johnston
917733b393 feat: add set_interval_with_handle and deprecate set_interval (#709) 2023-03-21 17:12:34 -04:00
martin frances
b3c5982ac8 clippy: less .clone() calls, simpler pointer passing. (#707) 2023-03-21 17:12:34 -04:00
Alexis Fontaine
3da1b72a3f fix: view! macro not compiling with a non-default scope name (#704) 2023-03-21 17:12:34 -04:00
Greg Johnston
e65bde9a5a feat: add a debounce helper for event listeners (#691) 2023-03-21 17:12:34 -04:00
Elliot Waite
b6b4f51b11 feat: add request_animation_frame_with_handle and request_idle_callback_with_handle (#698) 2023-03-21 17:12:34 -04:00
Greg Johnston
396a3506ff fix: ignore view markers in DynChild hydration (closes issue #697) (#703) 2023-03-21 17:12:34 -04:00
Greg Johnston
1bde018ca0 fix issues in release mode (closes #700) (#701) 2023-03-21 17:12:34 -04:00
Greg Johnston
f7489054ba docs: beginning work on router docs (#682) 2023-03-21 17:12:34 -04:00
Elliot Waite
404a64943f examples: remove duplicate console_error_panic_hook::set_once() calls (#692) 2023-03-21 17:12:34 -04:00
Greg Johnston
6d16a3feb5 feat: support diffing inside component children in hot-reload (#690) 2023-03-21 17:12:34 -04:00
Greg Johnston
49932fda39 fix: hydration errors with <Suspense/> inside components in SSR mode (#688) 2023-03-21 17:12:34 -04:00
Vassil "Vasco" Kolarov
50a31977fd examples: added example using Tailwind, CSR (only) and Trunk (#666) 2023-03-21 17:12:34 -04:00
Greg Johnston
7639b941c6 fix: allow multiple <Suspense/> on same page during in-order or async rendering (#687) 2023-03-21 17:12:34 -04:00
ryndin32
9db245dec4 docs: typos (#685) 2023-03-21 17:12:34 -04:00
Brett Etter
de42efab11 Added IntoView for ReadSignal and RwSignal in the stable feature. (#677) 2023-03-21 17:12:34 -04:00
Greg Johnston
12e8428a84 Update README.md 2023-03-21 17:12:34 -04:00
Greg Johnston
f76f027ed5 fix: suppress spurious hydration warnings for tags in leptos_meta (#684) 2023-03-21 17:12:34 -04:00
Greg Johnston
959c99d7d8 fix: leaking stored values (#683) 2023-03-21 17:12:34 -04:00
Greg Johnston
12d4a93ac6 CI: add --release checks (#681) 2023-03-21 17:12:34 -04:00
Greg Johnston
421544cec4 feat: maintain order of sources and dependencies (#678) 2023-03-21 17:12:34 -04:00
Brett Etter
364b20ccce fix: release mode (#679) 2023-03-21 17:12:34 -04:00
Greg Johnston
8b2daf869b feat: new reactive system implementation (#637) 2023-03-21 17:12:34 -04:00
Greg Johnston
56f8f20d4a chore: apply cargo machete systematically (#671) 2023-03-21 17:12:34 -04:00
Greg Johnston
6d9cc626ff feat: <ActionForm/> improvements (#676) 2023-03-21 17:12:34 -04:00
Greg Johnston
633bcc5f5d v0.2.3: fix broken stable support (#670) 2023-03-21 17:12:34 -04:00
Greg Johnston
76a3ce8794 v0.2.2 (#667) 2023-03-21 17:12:34 -04:00
martin frances
ddd446fd16 clippy: signal_wrappers_read, was using .clone() when copy is available. (#665) 2023-03-21 17:12:34 -04:00
Elliot Waite
ab939ccf7f docs: typo fixes and other small changes to the docs (#662) 2023-03-21 17:12:34 -04:00
Greg Johnston
7f34135463 fix: apply patches to all instances of a view, not just the first one (#663) 2023-03-21 17:12:34 -04:00
Greg Johnston
c3c371b020 fix: text node issue in template macro (#661) 2023-03-21 17:12:34 -04:00
Charles Taylor
365ac41cad feat: impl Copy & Clone for MaybeSignal (#660) 2023-03-21 17:12:34 -04:00
Greg Johnston
71164c08aa feat: add fragment support for hot reloading and fix some stuff (#659) 2023-03-21 17:12:34 -04:00
Ben Wishovich
d19902b404 feat: provide Request<_> in context for Axum, enabling easier extractor use (#632) 2023-03-21 17:12:34 -04:00
martin frances
e4589551ca chore: cargo machete: leptos_macro - Removed unused crates. (#656) 2023-03-21 17:12:34 -04:00
Greg Johnston
dba6f9ee22 feat: impl IntoView for &Fragment (#655) 2023-03-21 17:12:34 -04:00
Pikhosh
6f7685ad3e fix: show console error instead warning for error! (#654) 2023-03-21 17:12:34 -04:00
ealmloff
2c944177d4 feat: make server functions work outside of WASM (#643) 2023-03-21 17:12:34 -04:00
Greg Johnston
d6e564105e docs: add create_effect chapter (#653) 2023-03-21 17:12:34 -04:00
zack.shen
35dcd12cfd docs: spelling error (#651) 2023-03-21 17:12:34 -04:00
martin frances
a9ba6ca930 chore: bumped typed-builder up to 0.14. (#648) 2023-03-21 17:12:34 -04:00
martin frances
d63e65cd53 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-21 17:12:34 -04:00
Greg Johnston
6bbbacc7cd chore: typo (closes issue #645) (#646) 2023-03-21 17:12:34 -04:00
Vanius Bittencourt
0746bc433c feat: refactor leptos_config to allow loading from string (#628) 2023-03-21 17:12:34 -04:00
martin frances
9e80837313 chore: cargo machete: Strip down leptos_server. (#644) 2023-03-21 17:12:34 -04:00
martin frances
70c9286626 chore: bump serde-wasm-bindgen to 0.5. (#639) 2023-03-21 17:12:34 -04:00
martin frances
a0e564e9be chore: <Form/> component Removed unused variables. (#640) 2023-03-21 17:12:34 -04:00
martin frances
84e21d58aa Bumped tower-http upto 0.4. (#638) 2023-03-21 17:12:34 -04:00
Greg Johnston
efab33beb1 fix: custom events (closes issue #641) (#642) 2023-03-21 17:12:34 -04:00
jo!
2028a95eed examples: add session_auth_axum (#589) 2023-03-21 17:12:34 -04:00
Greg Johnston
647f62ffa7 CI: split into three actions (#636) 2023-03-21 17:12:34 -04:00
erwanvivien
d5a39037e1 de-duplicate todomvc example (#634) 2023-03-21 17:12:34 -04:00
martin frances
020b793417 bump typed-builder to version 0.13. (#633) 2023-03-21 17:12:34 -04:00
jfloresremar
a7e94e3026 Update 04_iteration.md (#630) 2023-03-21 17:12:34 -04:00
IchHabeKeineNamen
e9541e6f60 docs: fix instruction typos (#631) 2023-03-21 17:12:34 -04:00
Greg Johnston
dbb8e6bde4 fix: boolean attributes in SSR (#629) 2023-03-21 17:12:34 -04:00
WafflePersonThing
b4a0d9363f 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-21 17:12:34 -04:00
Greg Johnston
18caac3b2e feat: hot reloading support for cargo-leptos (#592) 2023-03-21 17:12:34 -04:00
Greg Johnston
091e05e610 docs: add a chapter on async actions and create_action (#623) 2023-03-21 17:12:34 -04:00
Greg Johnston
8f712966e3 CI: exclude rkyv combos with other serialization traits (#622) 2023-03-21 17:12:34 -04:00
Greg Johnston
aeb601f560 fix: suppress warnings caused by resource loading in generate_route_list (closes #582) (#621) 2023-03-21 17:12:34 -04:00
Greg Johnston
86e8fadc6d feat: allow easier client-side form validation (closes #413) (#620) 2023-03-21 17:12:34 -04:00
Greg Johnston
0a3935aa16 docs: add patterns for global state (closes #245) (#619) 2023-03-21 17:12:34 -04:00
Greg Johnston
62ed91c984 tests: use check instead of build in CI for disk space (#616) 2023-03-21 17:12:34 -04:00
Greg Johnston
8ecb63728c feat: allow multiple class names in view! macro class = (closes #612) (#614) 2023-03-21 17:12:34 -04:00
Greg Johnston
fd01a8ce30 docs: improve "Getting Started" page (#618) 2023-03-21 17:12:34 -04:00
Greg Johnston
9338fc4928 add note about running Trunk from root 2023-03-21 17:12:34 -04:00
martin frances
7eed00ae0e chore: clippy - simplified conditional logic in transition.rs. (#615) 2023-03-21 17:12:34 -04:00
Roland Fredenhagen
7911c4b613 feat: support expressions in #[prop(default=...)] (#611) 2023-03-21 17:12:34 -04:00
Ivan Agafonov
65b1518d6b docs: updated error handling code (#610)
code is from already updated example
2023-03-21 17:12:34 -04:00
Sergei Gnezdov
80c2d3ffff docs: fix compilation error, Issue #608 (#609)
Compiler reports error
F may not live long enough
2023-03-21 17:12:34 -04:00
Greg Johnston
e2c0bd1ad8 publish framework-independent server_fn crate (#605) 2023-03-21 17:12:34 -04:00
Ivan Agafonov
9819f28b2c docs: use create_node_ref instead of NodeRef::new (#607)
Code in the example already updated by someone
2023-03-21 17:12:34 -04:00
ealmloff
00730007d0 feat: make server functions framework agnostic (#596) 2023-03-21 17:12:34 -04:00
Greg Johnston
92920c2726 fix: memory leak in streaming SSR (closes issue #590) (#601) 2023-03-21 17:12:34 -04:00
Qwox
30a858defe fix: set new value before resetting input (#604)
Co-authored-by: Qwox <qwox@qwox.com>
2023-03-21 17:12:34 -04:00
Artem Makoven
1d6bf78b93 Fix typo in 03_components.md 2023-03-21 17:12:34 -04:00
Ivan Agafonov
d26fa60268 typo
_cx replaced with cx
2023-03-21 17:12:34 -04:00
Greg Johnston
4a685bfcdd fix SSR tests 2023-03-21 17:12:34 -04:00
Greg Johnston
2fadd92856 fix: don't re-set attributes found in HTML during hydration (closes #597) 2023-03-21 17:12:34 -04:00
Greg Johnston
19d2a1dfa7 fix: restore SSR fast-path support 2023-03-21 17:12:34 -04:00
Greg Johnston
6c86700666 examples: include missing examples in CI (#598) 2023-03-21 17:12:34 -04:00
Brendon Otto
6cb378e02a example: update README.md (#595)
Incorrect framework referenced
2023-03-21 17:12:34 -04:00
Greg Johnston
2b68fa6bc0 fix: mouseenter and mouseleave do not bubble (#593) 2023-03-21 17:12:34 -04:00
Thomas Kratz
c18847eb55 fix: make counter test compile (#588) 2023-03-21 17:12:34 -04:00
Azz
d851803f13 feat: support rkyv encoding (#577) 2023-03-21 17:12:34 -04:00
Greg Johnston
c122bbf9fa perf: improvements to event delegation and element creation in <For/> (#579) 2023-03-21 17:12:34 -04:00
g-re-g
ac8201877a fix: correct scheme handling in router, and improve matching code by removing regexes (#569) 2023-03-21 17:12:34 -04:00
Greg Johnston
0bc02df77f v0.2.0 2023-03-21 17:12:34 -04:00
tanguy-lf
d210d53f48 examples: add ssr_mode_axum (#575) 2023-03-21 17:12:34 -04:00
Greg Johnston
6b6782e8b0 fix: <Transition/> with local_resource (closes #562) (#574) 2023-03-21 17:12:34 -04:00
Markus Kohlhase
bf06b63779 example: Login with API token (CSR only) (#523) 2023-03-21 17:12:34 -04:00
Remo
409ffdd85f chore: macro panic hygiene (#568) 2023-03-21 17:12:34 -04:00
SleeplessOne1917
9bb5fc9965 fix: <Meta/> component as_ property outputs correct attribute html (#573) 2023-03-21 17:12:34 -04:00
Denis Nazarov
5a8039b68d Relax Eq to PartialEq for create_slice() (#570)
Co-authored-by: Denis Nazarov <denis.nazarov@gmail.com>
2023-03-21 17:12:34 -04:00
Greg Johnston
4c57ba4518 fixes issue #565 (#566) 2023-03-21 17:12:33 -04:00
Greg Johnston
ac6013efcc fix: transition fallback (closes #562) (#563) 2023-03-21 17:12:33 -04:00
Fangdun Tsai
3c3282887a feat: viz integration (#506) 2023-03-21 17:12:33 -04:00
PolarMutex
f829d6412e feature: add class prop to <Html/> component (#554) 2023-03-21 17:12:33 -04:00
Greg Johnston
bbafdbdb08 fix: issue with local resources blocking <Suspense/> fragments from resolving (#561) 2023-03-21 17:12:33 -04:00
Greg Johnston
f1d7ab0e75 fix: remove unnecessary log (#560) 2023-03-21 17:12:33 -04:00
Greg Johnston
98a4e87830 docs: add create_resource, <Suspense/>, and <Transition/> (#559) 2023-03-21 17:12:33 -04:00
Greg Johnston
4f524a57d8 docs: add example of <ButtonC on:click/> syntax (#558) 2023-03-21 17:12:33 -04:00
Thomas Versteeg
229b08084b doc: fix button name in parent_child example (#555) 2023-03-21 17:12:33 -04:00
Greg Johnston
d483d09300 v0.2.0-beta (#557) 2023-03-21 17:12:33 -04:00
Ben Wishovich
4a0e60ec42 fix issue with redirects in server fns creating multiple Location headers (#550) 2023-03-21 17:12:33 -04:00
Ikko Eltociear Ashimine
f0767cb76c fix: typo in hydration docs(#552)
identifer -> identifier
2023-03-21 17:12:33 -04:00
Chrislearn Young
0ca96d1bfe fix: document docs typo (#553) 2023-03-21 17:12:33 -04:00
Greg Johnston
8566725347 change: pass Scope as argument into Resource::read() and Resource::with() (#542) 2023-03-21 17:12:33 -04:00
Greg Johnston
2df349afd6 fix: more work on hydration IDs with <Suspense/> (#545) 2023-03-21 17:12:33 -04:00
Greg Johnston
7092bf5a6d revert PR #538 (#544) 2023-03-21 17:12:33 -04:00
Greg Johnston
26df094513 revert accident 2023-03-21 17:12:33 -04:00
Greg Johnston
3e95440b2d fix example 2023-03-21 17:12:33 -04:00
Dmitrii Kuzmin
bfa6cb0a78 fix(examples): hackernews_axum styles href (#536) 2023-03-21 17:12:33 -04:00
jquesada2016
149a377497 fix: off-by-one error in <For/> (closes #533) (#538) 2023-03-21 17:12:33 -04:00
Greg Johnston
6ae08493b3 fix: building leptos_reactive in release mode (#540) 2023-03-21 17:12:33 -04:00
Greg Johnston
858db3a792 v0.2.0-alpha2 (#539) 2023-03-21 17:12:33 -04:00
jquesada2016
10a2d27599 change: move signal method implementations into traits in signal prelude (#490) 2023-03-21 17:12:33 -04:00
Sean Aye
757f6231ef fix compile of leptos dom (#535) 2023-03-21 17:12:33 -04:00
Greg Johnston
042cf7614e feature: in-order streaming and async rendering (#496) 2023-03-21 17:12:33 -04:00
Greg Johnston
d8b02a1369 Fix test import location 2023-03-21 17:12:33 -04:00
Greg Johnston
427aa3f4c6 What's in a name? 2023-03-21 17:12:33 -04:00
Greg Johnston
b81592cf34 fix: hydration IDs for elements following <Suspense/> (closes #527) (#531) 2023-03-21 17:12:33 -04:00
Greg Johnston
a7d28e233b feature: reintroduce limited template-node cloning w/ template macro (#526) 2023-03-21 17:12:33 -04:00
Greg Johnston
7aed95b29b fix: top-level SVG in view macro with new exports (#525) 2023-03-21 17:12:33 -04:00
Greg Johnston
d90061f28e change: tweak API of Errors and implement IntoIter (#522) 2023-03-21 17:12:33 -04:00
henrik
d9cfcdad7c feature: enable cargo-leptos to reload multiple CSS files (#524) 2023-03-21 17:12:33 -04:00
Greg Johnston
dce7baaea0 Reexport web-sys event types in leptos::ev to make it easier to type handlers (#521) 2023-03-21 17:12:33 -04:00
Greg Johnston
f6145e8f65 fix: correct namespace for Unit in empty views (closes #518) (#520) 2023-03-21 17:12:33 -04:00
martin frances
7a93c3d3db leptos_dom erros.rs remove<E>() does not need to be generic. (#516)
* leptos_dom erros.rs remove<E>() does not need to be generic.

* fixed up errors.remove().
2023-03-21 17:12:33 -04:00
Greg Johnston
af4ae39a08 0.2.0-alpha (#515) 2023-03-21 17:12:33 -04:00
Greg Johnston
6ab2fef787 remove .unwrap() from redirect in Actix integration (#514) 2023-03-21 17:12:33 -04:00
IcosaHedron
0eaadaf391 do not unwrap use_context in integrations axum redirect (#513) 2023-03-21 17:12:33 -04:00
Greg Johnston
2a0ba3d884 CI: fix Wasm testing (#511) 2023-03-21 17:12:33 -04:00
Greg Johnston
02badcd23f fix: SSR export in Wasm mode (#512) 2023-03-21 17:12:33 -04:00
Greg Johnston
e4863bdba0 fix: import in leptos_dom and add Wasm build to CI for regressions (#510) 2023-03-21 17:12:33 -04:00
g-re-g
e601ab4949 Typos and a small cleanup (#509) 2023-03-21 17:12:33 -04:00
Greg Johnston
7362e1878a change: reorganize module exports and reexports (#503) 2023-03-21 17:12:33 -04:00
Greg Johnston
f8e84657b7 Add docs on testing (closes #489) (#508) 2023-03-21 17:12:33 -04:00
Greg Johnston
615ae14eec fix: <For/> in todomvc example (#504) 2023-03-21 17:12:33 -04:00
Greg Johnston
2f7c192835 docs: further additions (#505) 2023-03-21 17:12:33 -04:00
Greg Johnston
cbead2f02a apply new formatting everywhere (#502) 2023-03-21 17:12:33 -04:00
jquesada2016
517f4d423f chore: add workspace rustfmt.tml (#483) 2023-03-21 17:12:33 -04:00
Greg Johnston
7a88eae100 fix: proper disposal of nested route scopes (#499) 2023-03-21 17:12:33 -04:00
g-re-g
dbcfb023fc Allow literal string as class in view macro (#500) 2023-03-21 17:12:33 -04:00
Greg Johnston
10af646f37 examples: remove unused index.html (#497) 2023-03-21 17:12:33 -04:00
martin frances
03b7bd890a chore: remove unused .clone() call in <Suspense/>. (#486) 2023-03-21 17:12:33 -04:00
Greg Johnston
19985204c9 Handle <ErrorBoundary/> hydration correctly (closes #456) 2023-03-21 17:12:33 -04:00
Greg Johnston
c4364fa6d3 Correctly handle custom elements in SSR 2023-03-21 17:12:33 -04:00
Greg Johnston
2b6c8bbbfb Add error boundary example to list 2023-03-21 17:12:33 -04:00
Greg Johnston
ff67f64bcc Basic error boundary example 2023-03-21 17:12:33 -04:00
Greg Johnston
5a3bd9484c Update README.md 2023-03-21 17:12:33 -04:00
Greg Johnston
5c80182498 change: add Scope to view function in <For/> to avoid memory "leak" (#492) 2023-03-21 17:12:33 -04:00
Greg Johnston
475265acf8 missing ; 2023-03-21 17:12:33 -04:00
Greg Johnston
619260cf45 fix: fix debug_warn behavior in reactive crate and remove log dependency (#491) 2023-03-21 17:12:33 -04:00
jquesada2016
7e89eac267 change: NodeRef<HtmlElement<Div>> generics to NodeRef<Div> (#481) 2023-03-21 17:12:33 -04:00
Greg Johnston
17ee674b5c fix: typed route params with #[derive(Params)] (#488) 2023-03-21 17:12:33 -04:00
Greg Johnston
97ff7aa4f9 Fix inner_html in SSR (#487) 2023-03-21 17:12:33 -04:00
martin frances
3b0625f457 Minor: Clippy router now uses types OnFormData and OnResponse. (#484) 2023-03-21 17:12:33 -04:00
Greg Johnston
34c8a5b49a fix: errors on 404 page in axum_errors example (#485) 2023-03-21 17:12:33 -04:00
Jan
b51885e014 Better styling for router related components (#477) 2023-03-21 17:12:33 -04:00
Greg Johnston
4bc23f2828 remove unnecessary "openssl" feature from Actix examples (#480) 2023-03-21 17:12:33 -04:00
Greg Johnston
cca606527f Create README.md 2023-03-21 17:12:33 -04:00
Greg Johnston
93e0fc9379 docs: (in-progress) new tutorial/guide format with integrated CodeSandboxes (#375) 2023-03-21 17:12:33 -04:00
Greg Johnston
af0efa03ce Remove old book 2023-03-21 17:12:33 -04:00
Greg Johnston
24f4cecfbb fix: adding/removing errors from <ErrorBoundary/> (#478) 2023-03-21 17:12:33 -04:00
Greg Johnston
3d29fbc1ce fix: cargo doc in projects using #[server] (#476) 2023-03-21 17:12:33 -04:00
Greg Johnston
935d266f55 fix: correct out-of-order streaming behavior (#475) 2023-03-21 17:12:33 -04:00
g-re-g
b3369c6699 impl From<&str> for MaybeSignal<String> (#472) 2023-03-21 17:12:33 -04:00
Ben Wishovich
38a175a744 fix: convert site_address to site_addr to match cargo-leptos (#462) 2023-03-21 17:12:33 -04:00
Greg Johnston
edeb1dcf1e fix: fix node_ref in SSR (#471) 2023-03-21 17:12:33 -04:00
Greg Johnston
bd2baef127 fix: don't override element event listeners with component event listeners (closes #461) (#470) 2023-03-21 17:12:33 -04:00
Roland Fredenhagen
becf8a00cc error on non meta input for prop attribute (#469) 2023-03-21 17:12:33 -04:00
John Funk
b9a706a2bd Add simple icon logo (#468) 2023-03-21 17:12:33 -04:00
Greg Johnston
9ede00ca5d feature: add isomorphic <Redirect/> component (closes #412) (#466) 2023-03-21 17:12:33 -04:00
Roland Fredenhagen
381083121e impl Default for MaybeSignal (#464) 2023-03-21 17:12:33 -04:00
Greg Johnston
530f552616 docs: note about optional fallback (closes #406) (#463) 2023-03-21 17:12:33 -04:00
Jan
d092cdbaae Do it on an other branch (#460) 2023-03-21 17:12:33 -04:00
Greg Johnston
3fda12267f perf: further reduce WASM binary size by ~5-7% (#459)
* Update `leptos_router` docs
* Further reducing WASM bundle sizes
2023-03-21 17:12:33 -04:00
g-re-g
725954f784 Derive debug in server macro (#458) 2023-03-21 17:12:33 -04:00
Greg Johnston
4cb39d4bef docs: add new Children types to macro docs (#454) 2023-03-21 17:12:33 -04:00
Odiseo
c876cc9d65 fix: typo in leptos_config description (#455) 2023-03-21 17:12:33 -04:00
Greg Johnston
bc52e7e106 fix: stack overflow in with nested outlet (closes #452) (#453) 2023-03-21 17:12:33 -04:00
martin frances
0fc1ec1c94 Clippy: "{input} is not a supported environment. (#451) 2023-03-21 17:12:33 -04:00
Greg Johnston
eaf955b3ea fix: leptos_router hydration issues (#450) 2023-03-21 17:12:33 -04:00
Tobias Goulden Schultz
248a992ea2 fix: update leptos dependencies to point to the same workspace as other examples (#449) 2023-03-21 17:12:33 -04:00
Greg Johnston
35296b057f feature: allow on: event listeners on <Component/> nodes (#448) 2023-03-21 17:12:32 -04:00
Greg Johnston
850b129a0c fix: successfully pass context to nested routes via <Outlet/> (#447) 2023-03-21 17:12:32 -04:00
Gentle
fe3ce84200 use latest tokio in leptos_axum (#443) 2023-03-21 17:12:32 -04:00
martin frances
f7efd4d4ef router: Machete - Removed unused deps. (#442) 2023-03-21 17:12:32 -04:00
martin frances
d07d836ecb leptos_macro: Machete - Removed unused deps. (#441) 2023-03-21 17:12:32 -04:00
martin frances
b4a0fe85aa leptos-server: Removed dependecy on log, linear-map, rmp-serde. (#439) 2023-03-21 17:12:32 -04:00
g-re-g
bec11fead9 Dedup from_str implementations for Env (#426) 2023-03-21 17:12:32 -04:00
Greg Johnston
8eb60a2197 fix: correct behavior of <Show/> so it renders correctly when toggling between conditions multiple times, without rerendering on every change (#436) 2023-03-21 17:12:32 -04:00
Greg Johnston
21ddad1411 Fix top-level SVG elements in SSR (#435) 2023-03-21 17:12:32 -04:00
Greg Johnston
459fe54f55 Switch examples to check instead of build (for CI resources) and add missing examples (#437) 2023-03-21 17:12:32 -04:00
Greg Johnston
eb84f198af docs: Document inner_html attribute (#429) 2023-03-21 17:12:32 -04:00
Greg Johnston
b6bc7a070d Make RouteDefinition public (#430) 2023-03-21 17:12:32 -04:00
IcosaHedron
760a3574d0 Several Minor Updates on Examples (#427) 2023-03-21 17:12:32 -04:00
jquesada2016
a4c3292215 fixes cx not found on components marked with #[component(transparent)] (#423) 2023-03-21 17:12:32 -04:00
Greg Johnston
8670a36eeb fix: Make all fragment rendering lazy (closes #299 and #421) (#425)
Make all fragment rendering lazy (closes #299 and #421)
2023-03-21 17:12:32 -04:00
Greg Johnston
e5adb2ef0c fix: HTML entity issues in axum_errors example (#424) 2023-03-21 17:12:32 -04:00
Ben Wishovich
0378e0b077 Update ErrorBoundary to use miette::Diagnostic instead of Error, and various other tweaks (#401)
* Switch RwLock to parking_lot so they are no longer async
* cleanup todo_app_sqlite_axum
* add errors_axum example

---------

Co-authored-by: Indrazar <110272232+Indrazar@users.noreply.github.com>
2023-03-21 17:12:32 -04:00
starmaker
7301a24918 Implemented update_returning for StoredValue (#419) 2023-03-21 17:12:32 -04:00
Greg Johnston
f15f2365ac Fix issues with attribute names in SSR (#418) 2023-03-21 17:12:32 -04:00
Bruno De Simone
910aa6d993 Add leptos_routes functions for integrations (#415)
* added leptos_routes_with_context

* added leptos_routes_with_handler for axum integration
2023-03-21 17:12:32 -04:00
Ben Wishovich
9c9218c346 Switch RwLock to parking_lot so they are no longer async (#414) 2023-03-21 17:12:32 -04:00
Greg Johnston
9c55b35f6a Fix <option> and <use> top-level types in SSR (#416) 2023-03-21 17:12:32 -04:00
martin frances
05b1ce50d9 Escape <HTML> and <BODY> tokens in documentation markup. (#410) 2023-03-21 17:12:32 -04:00
martin frances
ed4815dd0e Minor: Bump typed-builder from 0.11 to 0.12. (#409) 2023-03-21 17:12:32 -04:00
Greg Johnston
db3c041e2f Add <Html/> and <Body/> components in leptos_meta (#407)
Closes #376.
2023-03-21 17:12:32 -04:00
Greg Johnston
d7c5cd8f27 oops 2023-03-21 17:12:32 -04:00
Greg Johnston
1c235e75df escape attributes 2023-03-21 17:12:32 -04:00
Greg Johnston
5c2381c675 Fixes boolean attributes in SSR (closes #405) 2023-03-21 17:12:32 -04:00
Greg Johnston
0e0fbf43f4 Revert "fix: Fixes boolean attributes in HTML fast-path (closes issue #405)"
This reverts commit 2ecb345a79.
2023-03-21 17:12:32 -04:00
Greg Johnston
0952dcd2d3 fix: Fixes boolean attributes in HTML fast-path (closes issue #405) 2023-03-21 17:12:32 -04:00
Greg Johnston
78d884db00 Add Children type alias 2023-03-21 17:12:32 -04:00
Greg Johnston
c8ff557a1c Fix labels in parent_child README 2023-03-21 17:12:32 -04:00
Greg Johnston
5e469039eb 0.1.3 2023-03-21 17:12:32 -04:00
Greg Johnston
00782697ef Missing web-sys types 2023-03-21 17:12:32 -04:00
Greg Johnston
6ad325fc6b Fix <ErrorBoundary/> removal behavior 2023-03-21 17:12:32 -04:00
Greg Johnston
a16becddcd Docs for <Show/> component 2023-03-21 17:12:32 -04:00
Greg Johnston
487a87ce49 Docs for <ErrorBoundary/> 2023-03-21 17:12:32 -04:00
Greg Johnston
e7184ee3f3 Fix hydration issue related to WASM size reduction 2023-03-21 17:12:32 -04:00
Greg Johnston
f46106e606 cargo fmt 2023-03-21 17:12:32 -04:00
Greg Johnston
1f5f8c03c2 clippy stuff 2023-03-21 17:12:32 -04:00
Greg Johnston
2a4c3f00d8 Make helpers into concrete functions for WASM binary size purposes 2023-03-21 17:12:32 -04:00
Greg Johnston
12affc3d98 Use a concrete helper function to generate elements 2023-03-21 17:12:32 -04:00
Thomas Queiroz
d923bf868a Fix gtk example 2023-03-21 17:12:32 -04:00
Greg Johnston
301e4d8288 Missing Storage dependency (now that gloo is gone) 2023-03-21 17:12:32 -04:00
Greg Johnston
fd2a074693 Remove gloo dependency in leptos_dom 2023-03-21 17:12:32 -04:00
Martin
9416517e0e BugFix, ch03 properly construct the "input_element". 2023-03-21 17:12:32 -04:00
Martin
b5a9131c8e doc/book updated leptos version. 2023-03-21 17:12:32 -04:00
Greg Johnston
a8d6b9aca3 Correctly set pending state with ActionForm 2023-03-21 17:12:32 -04:00
Greg Johnston
8db98f26d2 ActionForm should clear input as Action::dispatch() does 2023-03-21 17:12:32 -04:00
Greg Johnston
50eff91072 Fix missing docs error (#389) 2023-03-21 17:12:32 -04:00
Greg Johnston
7d6e3c99bc Check uniqueness of server function names at registration time (#388)
* Check uniqueness of server function names at registration time, and stop leaking src file path in release mode

* Fix missing dev-dependency
2023-03-21 17:12:32 -04:00
Greg Johnston
283dfdd075 Fix a large number of small issues in docs (#386)
* Fix example links in docs

* Restore missing CSR READMEs

* Document need to enable features on `leptos_router` and `leptos_meta`

* Add "Is it production ready?" to FAQs

* Document which types are provided as contexts in server integrations

* Fix broken links and other issues in docs
2023-03-21 17:12:32 -04:00
Greg Johnston
eaeca26d12 Allow unused cx in server fn arguments (#385)
* Suppress warning for unused `cx` in server function arguments
2023-03-21 17:12:32 -04:00
Greg Johnston
c939182c68 implements From<Signal<T>> for MaybeSignal<T> (#384) 2023-03-21 17:12:32 -04:00
Greg Johnston
a496f3c5a9 Replace site-address with site-addr in cargo-leptos example Cargo.toml files 2023-03-21 17:12:32 -04:00
Gentle
b6a8171f4c leptos_axum::handle_server_fns was also duplicated (#383) 2023-03-21 17:12:32 -04:00
Roland Fredenhagen
aff190d41b added hgroup element (#379) 2023-03-21 17:12:32 -04:00
Gentle
88e98c0f7e cloning is not needed here (#381) 2023-03-21 17:12:32 -04:00
Gentle
ac92b63e4c refactor to eliminate duplicate code (#380) 2023-03-21 17:12:32 -04:00
Greg Johnston
5b116596bf Fix context in outlets (#374)
* Add `Scope::parent()` to make access to parent `Scope` possible.

* Handle context properly in nested routes
2023-03-21 17:12:32 -04:00
Markus Kohlhase
8e2d5598b5 Add a counter example that does not use macros (#373) 2023-03-21 17:12:32 -04:00
Ben Wishovich
4023e16507 Make Errors Sync (#372) 2023-03-21 17:12:32 -04:00
IcosaHedron
d4da935a0c Fix CSR with Trunk on hackernews example, remove CSR option from isomorphic example (#369)
* Fix CSR with Trunk on hackernews example

* Update isomorphic example to remove CSR from Readme
2023-03-21 17:12:32 -04:00
Markus Kohlhase
1d5ed41e1d Replace urlencoding with percent-encoding (#365)
Motivation: `percent-encoding` is from the Servo team and part of the `url` crate.
2023-03-21 17:12:32 -04:00
Ben Wishovich
22b8640b37 Add <Show/> component to avoid rerendering of closures and tweak ErrorBoundary (#363)
Add once_cell to leptos, and add Show component! Modify ErrorBoundary to
take a closure that implements IntoView, not View
2023-03-21 17:12:32 -04:00
Greg Johnston
e5998eb321 Reorganize docs re: snake-case names 2023-01-23 09:09:35 -05:00
Greg Johnston
65b0e1265b clippy 2023-01-23 09:09:26 -05:00
385 changed files with 26647 additions and 9653 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:
@@ -30,17 +30,16 @@ jobs:
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
- 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,37 +4,43 @@ 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",
"router",
# book
"docs/book/project/ch02_getting_started",
"docs/book/project/ch03_building_ui",
"docs/book/project/ch04_reactivity",
]
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.1.1"
version = "0.2.4"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.1.1" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.1" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.1" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.1" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.1" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.1" }
leptos_router = { path = "./router", version = "0.1.1" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.1" }
leptos = { path = "./leptos", default-features = false, version = "0.2.4" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.4" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.4" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.4" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.4" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.4" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.4" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.4" }
leptos_router = { path = "./router", version = "0.2.4" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.4" }
[profile.release]
codegen-units = 1

View File

@@ -7,34 +7,63 @@
# make tasks run at the workspace root
default_to_workspace = false
[tasks.ci]
dependencies = ["build", "build-examples", "test"]
[tasks.build]
clear = true
dependencies = ["build-all"]
[tasks.build-all]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.build-examples]
[tasks.check]
clear = true
dependencies = [
{ name = "build", path = "examples/counter" },
{ name = "build", path = "examples/counter_isomorphic" },
{ name = "build", path = "examples/counters" },
{ name = "build", path = "examples/counters_stable" },
{ name = "build", path = "examples/fetch" },
{ name = "build", path = "examples/hackernews" },
{ name = "build", path = "examples/hackernews_axum" },
{ name = "build", path = "examples/parent_child" },
{ name = "build", path = "examples/router" },
{ name = "build", path = "examples/tailwind" },
{ name = "build", path = "examples/todo_app_sqlite" },
{ name = "build", path = "examples/todo_app_sqlite_axum" },
{ name = "build", path = "examples/todomvc" },
"check-all",
"check-wasm",
"check-all-release",
"check-wasm-release",
]
[tasks.check-all]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-wasm]
clear = true
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/counters" },
{ 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]

View File

@@ -78,9 +78,9 @@ 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-alpha", 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
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)
for examples of the correct API.
@@ -99,6 +99,28 @@ 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.
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
With 0.1 the APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
2. **Are there bugs?**
Yes, Im sure there are. You can see from the state of our issue tracker over time that there arent that _many_ bugs and theyre usually resolved pretty quickly. But for sure, there may be moments where you encounter something that requires a fix at the framework level, which may not be immediately resolved.
3. **Am I a consumer or a contributor?**
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) dont have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if youre willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
### Can I use this for native GUI?
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:

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 +1 @@
book
book

14
docs/book/README.md Normal file
View File

@@ -0,0 +1,14 @@
This project contains the core of a new introductory guide to Leptos.
It is built using `mdbook`. You can view a local copy by installing `mdbook`
```bash
cargo install mdbook
```
and run the book with
```
mdbook serve
```
It should be available at `http://localhost:3000`.

View File

@@ -1,16 +0,0 @@
[book]
authors = ["Greg Johnston"]
language = "en"
multilingual = false
src = "src"
title = "The Leptos Guide"
[preprocessor]
[preprocessor.mermaid]
command = "mdbook-mermaid"
[output]
[output.html]
additional-js = ["mermaid.min.js", "mermaid-init.js"]

View File

@@ -1 +0,0 @@
mermaid.initialize({startOnLoad:true});

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
[package]
name = "ch02_getting_started"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Leptos • Todos</title>
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
<link data-trunk rel="rust" data-wasm-opt="z" />
</head>
<body></body>
</html>

View File

@@ -1,5 +0,0 @@
use leptos::*;
fn main() {
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
}

View File

@@ -1,7 +0,0 @@
[package]
name = "ch03_building_ui"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Leptos • Todos</title>
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
<link data-trunk rel="rust" data-wasm-opt="z" />
</head>
<body></body>
</html>

View File

@@ -1,39 +0,0 @@
use leptos::*;
fn main() {
mount_to_body(|cx| {
let name = "gbj";
let userid = 0;
let _input_element: Element;
view! {
cx,
<main>
<h1>"My Tasks"</h1> // text nodes are wrapped in quotation marks
<h2>"by " {name}</h2>
<input
type="text" // attributes work just like they do in HTML
name="new-todo"
prop:value="todo" // `prop:` lets you set a property on a DOM node
value="initial" // side note: the DOM `value` attribute only sets *initial* value
// this is very important when working with forms!
_ref=_input_element // `_ref` stores tis element in a variable
/>
<ul data-user=userid> // attributes can take expressions as values
<li class="todo my-todo" // here we set the `class` attribute
class:completed=true // `class:` also lets you toggle individual classes
on:click=|_| todo!() // `on:` adds an event listener
>
"Buy milk."
</li>
<li class="todo my-todo" class:completed=false>
"???"
</li>
<li class="todo my-todo" class:completed=false>
"Profit!!!"
</li>
</ul>
</main>
}
})
}

View File

@@ -1,7 +0,0 @@
[package]
name = "ch04_reactivity"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"

View File

@@ -1,28 +0,0 @@
use leptos::*;
fn main() {
run_scope(create_runtime(), |cx| {
// signal
let (count, set_count) = create_signal(cx, 1);
// derived signal
let double_count = move || count() * 2;
// memo
let memoized_square = create_memo(cx, move |_| count() * count());
// effect
create_effect(cx, move |_| {
println!(
"count =\t\t{} \ndouble_count = \t{}, \nsquare = \t{}",
count(),
double_count(),
memoized_square()
);
});
set_count(1);
set_count(2);
set_count(3);
});
}

View File

@@ -1,10 +1,19 @@
# Introduction
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework. Together, well build a simple todo app—first as a client-side app, then as a full-stack app.
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
It will walk through the fundamental concepts you need to build applications,
beginning with a simple application rendered in the browser, and building toward a
full-stack application with server-side rendering and hydration.
The guide doesnt assume you know anything about fine-grained reactivity or the details of modern Web frameworks. It does assume you are familiar with the Rust programming language, HTML, CSS, and the DOM and other Web APIs.
The guide doesnt assume you know anything about fine-grained reactivity or the
details of modern Web frameworks. It does assume you are familiar with the Rust
programming language, HTML, CSS, and the DOM and basic Web APIs.
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript) and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities to other frameworks like React (JavaScript), Yew (Rust), and Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to understand Leptos.
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
understand Leptos.
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).

View File

@@ -1,37 +1,70 @@
# Getting Started
> The code for this chapter can be found [here](https://github.com/leptos-rs/leptos/tree/main/docs/book/project/ch02_getting_started).
There are two basic paths to getting started with Leptos:
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/leptos-rs/leptos/tree/main/examples) do. (Trunk is a simple build tool that includes a dev server.)
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
cargo install --lock trunk
cargo install trunk
```
Create a basic Rust binary project
```bash
cargo init leptos-todo
cargo init leptos-tutorial
```
Add `leptos` as a dependency to your `Cargo.toml` with the `csr` featured enabled. (That stands for “client-side rendering.” Well talk more about Leptoss support for server-side rendering and hydration later.)
> 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
> ```
```toml
leptos = "0.0"
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos
```
Youll want to set up a basic `index.html` with the following content:
Create a simple `index.html` in the root of the `leptos-tutorial` directory
```html
{{#include ../project/ch02_getting_started/index.html}}
<!DOCTYPE html>
<html>
<head></head>
<body></body>
</html>
```
Lets start with a very simple `main.rs`
And add a simple “Hello, world!” to your `main.rs`
```rust
{{#include ../project/ch02_getting_started/src/main.rs}}
use leptos::*;
fn main() {
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

@@ -1,49 +0,0 @@
# Templating: Building User Interfaces
> The code for this chapter can be found [here](https://github.com/leptos-rs/leptos/tree/main/docs/book/project/ch03_building_ui).
## RSX and the `view!` macro
Okay, that “Hello, world!” was a little boring. Were going to be building a todo app, so lets look at something a little more complicated.
As you noticed in the first example, Leptos lets you describe your user interface with a declarative `view!` macro. It looks something like this:
```
view! {
cx, // this is the "reactive scope": more on that in the next chapter
<p>"..."</p> // this is some HTML-ish stuff
}
```
The “HTML-ish stuff” is what we call “RSX”: XML in Rust. (You may recognize the similarity to JSX, which is the mixed JavaScript/XML syntax used by frameworks like React.)
Heres a more in-depth example:
```rust
{{#include ../project/ch03_building_ui/src/main.rs}}
```
Youll probably notice a few things right away:
1. Elements without children need to be explicit closed with a `/` (`<input/>`, not `<input>`)
2. Text nodes are formatted as strings, i.e., wrapped in quotation marks (`"My Tasks"`)
3. Dynamic blocks can be inserted as children of elements, if wrapped in curly braces (`<h2>"by " {name}</h2>`)
4. Attributes can be given Rust expressions as values. This could be a string literal as in HTML (`<input type="text" .../>)` or a variable or block (`data-user=userid` or `on:click=move |_| { ... }`)
5. Unlike in HTML, whitespace is ignored and should be manually added (its `<h2>"by " {name}</h2>`, not `<h2>"by" {name}</h2>`; the space between `"by"` and `{name}` is ignored.)
6. Normal attributes work exactly like you'd think they would.
7. There are also special, prefixed attributes.
- `class:` lets you make targeted updates to a single class
- `on:` lets you add an event listener
- `prop:` lets you set a property on a DOM element
- `_ref` stores the DOM element youre creating in a variable
> You can find more information in the [reference docs for the `view!` macro](https://docs.rs/leptos/0.0.15/leptos/macro.view.html).
## But, wait...
This example shows some parts of the Leptos templating syntax. But its completely static.
How do you actually make the user interface interactive?
In the next chapter, well talk about “fine-grained reactivity,” which is the core of the Leptos framework.

View File

@@ -1,240 +0,0 @@
# Reactivity
## What is reactivity?
A few months ago, I completely baffled a friend by trying to explain what I was working on. “You have two variables, right? Call them `a` and `b`. And then you have a third variable, `c`. And when you update `a` or `b`, the value of `c` just _automatically changes_. And it changes _on the screen_! Automatically!”
“Isnt that just... how computers work?” she asked me, puzzled. If your programming experience is limited to something like spreadsheets, its a reasonable enough assumption. This is, after all, how math works.
But you know this isn't how ordinary imperative programming works.
```rust,should_panic
let mut a = 0;
let mut b = 0;
let c = a + b;
assert_eq!(c, 0); // sanity check
a = 2;
b = 2;
// now c = 4, right?
assert_eq!(c, 4); // nope. we all know this is wrong!
```
But thats _exactly_ how reactive programming works.
```rust
use leptos::*;
run_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();
assert_eq!(c(), 0); // yep, still true
set_a(2);
set_b(2);
assert_eq!(c(), 4); // ohhhhh yeah.
});
```
Hopefully, this makes some intuitive sense. After all, `c` is a closure. Calling it again causes it to access its values a second time. This isnt _that_ cool.
```rust
use leptos::*;
run_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();
create_effect(cx, move |_| {
println!("c = {}", c()); // prints "c = 0"
});
set_a(2); // prints "c = 2"
set_b(2); // prints "c = 4"
});
```
This examples a little different. [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) defines a “side effect,” a bridge between the reactive system of signals and the outside world. Effects synchronize the reactive system with everything else: the console, the filesystem, an HTTP request, whatever.
Because the closure `c` is called within the effect and in turns calls the signals `a` and `b`, the effect automatically subscribes to the signals `a` and `b`. This means that whenever `a` or `b` is updated, the effect will re-run, logging the value again.
You can picture the reactive graph for this system like this:
```mermaid
graph TD;
A-->C;
B-->C;
C-->Effect;
```
This is the foundation on which _everything_ else is built.
## Reactive Primitives
### Overview
The reactive system is built on the interaction between these two halves: **signals** and **effects**. When a signal is called inside an effect, the effect automatically subscribes to the signal. When a signals value is updated, it automatically notifies all its subscribers, and they re-run.
The following simple example contains most of the core reactive concepts:
```rust
{{#include ../project/ch04_reactivity/src/main.rs}}
```
This creates a reactive graph like this:
```mermaid
graph TD;
count-->double_count;
count-->memoized_square;
count-->effect;
double_count-->effect;
memoized_square-->effect;
```
**Signals** are reactive values created using [`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html) or [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html).
**Derived Signals** computations in ordinary closures that rely on other signals. The computation re-runs whenever you access its value.
**Memos** are computations that are memoized with [create_memo](https://docs.rs/leptos/latest/leptos/fn.create_memo.html). Memos only re-run when one of their signal dependencies has changed.
And **effects** (created with [create_effect](<(https://docs.rs/leptos/latest/leptos/fn.create_effect.html)>) synchronize the reactive system with something outside it.
The rest of this chapter will walk through each of these concepts in more depth.
### Signals
A **signal** is a piece of data that may change over time, and notifies other code when it has changed. This is the core primitive of Leptoss reactive system.
Creating a signal is very simple. You call `create_signal`, passing in the reactive scope and the default value, and receive a tuple containing a `ReadSignal` and a `WriteSignal`.
```rust
let (value, set_value) = create_signal(cx, 0);
```
> If youve used signals in Sycamore or Solid, observables in MobX or Knockout, or a similar primitive in reactive library, you probably have a pretty good idea of how signals work in Leptos. If youre familiar with React, Yew, or Dioxus, you may recognize a similar pattern to their `use_state` hooks.
#### `ReadSignal<T>`
The [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) half of this tuple allows you to get the current value of the signal. Reading that value in a reactive context automatically subscribes to any further changes. You can access the value by simply calling the `ReadSignal` as a function.
```rust
let (value, set_value) = create_signal(cx, 0);
// calling value() with return the current value of the signal,
// and automatically track changes if you're in a reactive context
assert_eq!(value(), 0);
```
> Here, a **reactive context** means anywhere within an `Effect`. Leptoss templating system is built on top of its reactive system, so if youre reading the signals value within the template, the template will automatically subscribe to the signal and update exactly the value that needs to change in the DOM.
Calling a `ReadSignal` clones the value it contains. If thats too expensive, use [`ReadSignal::with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#method.with) to borrow the value and do whatever you need.
```rust
struct MySuperExpensiveStruct {
a: String,
b: StructThatsSuperExpensiveToClone
}
let (value, set_value) = create_signal(cx, MySuperExpensiveStruct::default());
// ❌ this is going to clone the `StructThatsSuperExpensiveToClone` unnecessarily!
let lowercased = move || value().a.to_lowercase();
// ✅ only use what we need
let lowercased = move || value.with(|value: &MySuperExpensiveStruct| value.a.to_lowercase());
```
#### `WriteSignal<T>`
The [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) half of this tuple allows you to update the value of the signal, which will automatically notify anything thats listening to the value that something has changed. If you simply call the `WriteSignal` as a function, its value will be set to the argument you pass. If you want to mutate the value in place instead of replacing it, you can call [`WriteSignal::update`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#method.update) instead.
```rust
// often you just want to replace the value
let (value, set_value) = create_signal(cx, 0);
set_value(1);
assert_eq!(value(), 1);
// sometimes you want to mutate something in place, like a Vec. Just call update()
let (items, set_items) = create_signal(cx, vec![0]);
set_items.update(|items: &mut Vec<i32>| items.push(1));
assert_eq!(items(), vec![1]);
```
> Under the hood, `set_value(1)` is just syntactic sugar for `set_value.update(|n| *n = 1)`.
#### `RwSignal<T>`
This kind of “read-write segregation,” in which the getter and the setter are stored in separate variables, may be familiar from the tuple-based ”hooks” pattern in libraries like React, Solid, Yew, or Dioxus. It encourages clear contracts between components. For example, if a child component only needs to be able to read a signal, but shouldnt be able to update it (and therefore trigger changes in other parts of the application), you can pass it only the `ReadSignal`.
Sometimes, however, you may prefer to keep the getter and setter combined in one variable. For example, its awkward and repetitive to store both halves of a signal in another data structure:
```rust
# use leptos::*;
// pretty repetitive
struct AppState {
count: ReadSignal<i32>,
set_count: WriteSignal<i32>,
name: ReadSignal<String>,
set_name: WriteSignal<String>
}
#[component]
fn App(cx: Scope) {
let (count, set_count) = create_signal(cx, 0);
let (name, set_name) = create_signal(cx, "Alice".to_string());
provide_context(cx, AppState {
count,
set_count,
name,
set_name
})
todo!()
}
```
Or maybe you just like to keep your getters and setters in one place.
In this case, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) and the [`RwSignal`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html) type. This returns a **R**ead-**w**rite Signal, which has the same [`get`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get), [`with`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.with), [`set`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.set), and [`update`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.update) functions as the `ReadSignal` and `WriteSignal` halves.
```rust
# use leptos::*;
// better
struct AppState {
count: RwSignal<i32>,
name: RwSignal<String>,
}
#[component]
fn App(cx: Scope) {
let count = create_rw_signal(cx, 0);
let name = create_rw_signal(cx, "Alice".to_string());
provide_context(cx, AppState {
count,
name,
})
todo!()
}
```
If you still want to hand off read-only access to another part of the app, you can get a `ReadSignal` with [`RwSignal::read_only()`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get).
### Derived Signals
(todo)
### Memos
(todo)
### Effects
(todo)

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

@@ -2,5 +2,47 @@
- [Introduction](./01_introduction.md)
- [Getting Started](./02_getting_started.md)
- [Templating: Building User Interfaces](./03_building_ui.md)
- [Reactivity: Making Things Interactive](./04_reactivity.md)
- [Building User Interfaces](./view/README.md)
- [A Basic Component](./view/01_basic_component.md)
- [Dynamic Attributes](./view/02_dynamic_attributes.md)
- [Components and Props](./view/03_components.md)
- [Iteration](./view/04_iteration.md)
- [Forms and Inputs](./view/05_forms.md)
- [Control Flow](./view/06_control_flow.md)
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.md)
- [Interlude: Reactivity and Functions](./interlude_functions.md)
- [Testing](./testing.md)
- [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]()
- [Actions]()
- [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

@@ -0,0 +1,76 @@
# Interlude: Reactivity and Functions
One of our core contributors said to me recently: “I never used closures this often
until I started using Leptos.” And its true. Closures are at the heart of any Leptos
application. It sometimes looks a little silly:
```rust
// a signal holds a value, and can be updated
let (count, set_count) = create_signal(cx, 0);
// a derived signal is a function that accesses other signals
let double_count = move || count() * 2;
let count_is_odd = move || count() & 1 == 1;
let text = move || if count_is_odd() {
"odd"
} else {
"even"
};
// an effect automatically tracks the signals it depends on
// and reruns when they change
create_effect(cx, move |_| {
log!("text = {}", text());
});
view! { cx,
<p>{move || text().to_uppercase()}</p>
}
```
Closures, closures everywhere!
But why?
## Functions and UI Frameworks
Functions are at the heart of every UI framework. And this makes perfect sense. Creating a user interface is basically divided into two phases:
1. initial rendering
2. updates
In a web framework, the framework does some kind of initial rendering. Then it hands control back over to the browser. When certain events fire (like a mouse click) or asynchronous tasks finish (like an HTTP request finishing), the browser wakes the framework back up to update something. The framework runs some kind of code to update your user interface, and goes back asleep until the browser wakes it up again.
The key phrase here is “runs some kind of code.” The natural way to “run some kind of code” at an arbitrary point in time—in Rust or in any other programming language—is to call a function. And in fact every UI framework is based on rerunning some kind of function over and over:
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 rerun
Thats what all our components are doing.
Take our typical `<SimpleCounter/>` example in its simplest form:
```rust
#[component]
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
let increment = move |_| set_value.update(|value| *value += 1);
view! { cx,
<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 rerun the smallest possible unit of your application in responsive to a change.
So remember two things:
1. Your component function is a setup function, not a render function: it only runs once.
2. For values in your view template to be reactive, they must be functions: either signals (which implement the `Fn` traits) or closures.

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.

180
docs/book/src/testing.md Normal file
View File

@@ -0,0 +1,180 @@
# Testing Your Components
Testing user interfaces can be relatively tricky, but really important. This article
will discuss a couple principles and approaches for testing a Leptos app.
## 1. Test business logic with ordinary Rust tests
In many cases, it makes sense to pull the logic out of your components and test
it separately. For some simple components, theres no particular logic to test, but
for many its worth using a testable wrapping type and implementing the logic in
ordinary Rust `impl` blocks.
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 num_remaining = move || todos.with(|todos| {
todos.iter().filter(|todo| !todo.completed).sum()
});
}
```
You could pull that logic out into a separate data structure and test it:
```rust
pub struct Todos(Vec<Todo>);
impl Todos {
pub fn num_remaining(&self) -> usize {
todos.iter().filter(|todo| !todo.completed).sum()
}
}
#[cfg(test)]
mod tests {
#[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 num_remaining = move || todos.with(Todos::num_remaining);
}
```
In general, the less of your logic is wrapped into your components themselves, the
more idiomatic your code will feel and the easier it will be to test.
## 2. Test components with `wasm-bindgen-test`
[`wasm-bindgen-test`](https://crates.io/crates/wasm-bindgen-test) is a great utility
for integrating or end-to-end testing WebAssembly apps in a headless browser.
To use this testing utility, you need to add `wasm-bindgen-test` to your `Cargo.toml`:
```toml
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
```
You should create tests in a separate `tests` directory. You can then run your tests in the browser of your choice:
```bash
wasm-pack test --firefox
```
> To see the full setup, check out the tests for the [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) example.
### Writing Your Tests
Most tests will involve some combination of vanilla DOM manipulation and comparison to a `view`. For example, heres a test [for the
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/mod.rs).
First, we set up the testing environment.
```rust
use wasm_bindgen_test::*;
use counter::*;
use leptos::*;
use web_sys::HtmlElement;
// tell the test runner to run tests in the browser
wasm_bindgen_test_configure!(run_in_browser);
```
Im going to create a simpler wrapper for each test case, and mount it there.
This makes it easy to encapsulate the test results.
```rust
// like marking a regular test with #[test]
#[wasm_bindgen_test]
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
);
```
Well use some manual DOM operations to grab the `<div>` that wraps
the whole component, as well as the `clear` button.
```rust
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
```
Now we can use ordinary DOM APIs to simulate user interaction.
```rust
// now let's click the `clear` button
clear.click();
```
You can test individual DOM element attributes or text node values. Sometimes
I like to test the whole view at once. We can do this by testing the elements
`outerHTML` against our expectations.
```rust
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
);
```
That test involved us manually replicating the `view` thats inside the component.
There's actually an easier way to do this... We can just test against a `<SimpleCounter/>`
with the initial value `0`. This is where our wrapping element comes in: Ill just test
the wrappers `innerHTML` against another comparison case.
```rust
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
}
```
This is only a very limited introduction to testing. But I hope its useful as you begin to build applications.
> For more, see [the testing section of the `wasm-bindgen` guide](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/index.html#testing-on-wasm32-unknown-unknown-with-wasm-bindgen-test).

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
# Forms and Inputs
Forms and form inputs are an important part of interactive apps. There are two
basic patterns for interacting with inputs in Leptos, which you may recognize
if youre familiar with React, SolidJS, or a similar framework: using **controlled**
or **uncontrolled** inputs.
## Controlled Inputs
In a "controlled input," the framework controls the state of the input
element. On every `input` event, it updates a local signal that holds the current
state, which in turn updates the `value` prop of the input.
There are two important things to remember:
1. The `input` event fires on (almost) every change to the element, while the
`change` event fires (more or less) when you unfocus the input. You probably
want `on:input`, but we give you the freedom to choose.
2. The `value` *attribute* only sets the initial value of the input, i.e., it
only updates the input up to the point that you begin typing. The `value`
*property* continues updating the input after that. You usually want to set
`prop:value` for this reason.
```rust
let (name, set_name) = create_signal(cx, "Controlled".to_string());
view! { cx,
<input type="text"
on:input=move |ev| {
// event_target_value is a Leptos helper function
// it functions the same way as event.target.value
// in JavaScript, but smooths out some of the typecasting
// necessary to make this work in Rust
set_name(event_target_value(&ev));
}
// the `prop:` syntax lets you update a DOM property,
// rather than an attribute.
prop:value=name
/>
<p>"Name is: " {name}</p>
}
```
## Uncontrolled Inputs
In an "uncontrolled input," the browser controls the state of the input element.
Rather than continuously updating a signal to hold its value, we use a
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
the input once when we want to get its value.
In this example, we only notify the framework when the `<form>` fires a `submit`
event.
```rust
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
let input_element: NodeRef<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.
```rust
let on_submit = move |ev: SubmitEvent| {
// stop the page from reloading!
ev.prevent_default();
// here, we'll extract the value from the input
let value = input_element()
// event handlers can only fire after the view
// is mounted to the DOM, so the `NodeRef` will be `Some`
.expect("<input> to exist")
// `NodeRef` implements `Deref` for the DOM element type
// this means we can call`HtmlInputElement::value()`
// to get the current value of the input
.value();
set_name(value);
};
```
Our `on_submit` handler will access the inputs value and use it to call `set_name`.
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
know it will already have been filled when we rendered the view, so its safe to
unwrap here.
We can then call `.value()` to get the value out of the input, because `NodeRef`
gives us access to a correctly-typed HTML element.
```rust
view! { cx,
<form on:submit=on_submit>
<input type="text"
value=name
node_ref=input_element
/>
<input type="submit" value="Submit"/>
</form>
<p>"Name is: " {name}</p>
}
```
The view should be pretty self-explanatory by now. Note two things:
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
This is because were just setting the initial value of the input, and letting
the browser control its state. (We could use `prop:value` instead.)
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
They are the same thing, but `node_ref` has better rust-analyzer support.)
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,285 @@
# Control Flow
In most applications, you sometimes need to make a decision: Should I render this
part of the view, or not? Should I render `<ButtonA/>` or `<WidgetB/>`? This is
**control flow**.
## A Few Tips
When thinking about how to do this with Leptos, its important to remember a few
things:
1. Rust is an expression-oriented language: control-flow expressions like
`if x() { y } else { z }` and `match x() { ... }` return their values. This
makes them very useful for declarative user interfaces.
2. For any `T` that implements `IntoView`—in other words, for any type that Leptos
knows how to render—`Option<T>` and `Result<T, impl Error>` _also_ implement
`IntoView`. And just as `Fn() -> T` renders a reactive `T`, `Fn() -> Option<T>`
and `Fn() -> Result<T, impl Error>` are reactive.
3. Rust has lots of handy helpers like [Option::map](https://doc.rust-lang.org/std/option/enum.Option.html#method.map),
[Option::and_then](https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then),
[Option::ok_or](https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or),
[Result::map](https://doc.rust-lang.org/std/result/enum.Result.html#method.map),
[Result::ok](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok), and
[bool::then](https://doc.rust-lang.org/std/primitive.bool.html#method.then) that
allow you to convert, in a declarative way, between a few different standard types,
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 rerun
when the signal they depend on changes, keeping the UI reactive.
## So What?
To connect the dots a little: this means that you can actually implement most of
your control flow with native Rust code, without any control-flow components or
special knowledge.
For example, lets start with a simple signal and derived signal:
```rust
let (value, set_value) = create_signal(cx, 0);
let is_odd = move || value() & 1 == 1;
```
> If you dont recognize whats going on with `is_odd`, dont worry about it
> too much. Its just a simple way to test whether an integer is odd by doing a
> bitwise `AND` with `1`.
We can use these signals and ordinary Rust to build most control flow.
### `if` statements
Lets say I want to render some text if the number is odd, and some other text
if its even. Well, how about this?
```rust
view! { cx,
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
}
```
An `if` expression returns its value, and a `&str` implements `IntoView`, so a
`Fn() -> &str` implements `IntoView`, so this... just works!
### `Option<T>`
Lets say we want to render some text if its odd, and nothing if its even.
```rust
let message = move || {
if is_odd() {
Some("Ding ding ding!")
} else {
None
}
};
view! { cx,
<p>{message}</p>
}
```
This works fine. We can make it a little shorter if wed like, using `bool::then()`.
```rust
let message = move || is_odd().then(|| "Ding ding ding!");
view! { cx,
<p>{message}</p>
}
```
You could even inline this if youd like, although personally I sometimes like the
better `cargo fmt` and `rust-analyzer` support I get by pulling things out of the `view`.
### `match` statements
Were still just writing ordinary Rust code, right? So you have all the power of Rusts
pattern matching at your disposal.
```rust
let message = move || {
match value() {
0 => "Zero",
1 => "One",
n if is_odd() => "Odd",
_ => "Even"
}
};
view! { cx,
<p>{message}</p>
}
```
And why not? YOLO, right?
## Preventing Over-Rendering
Not so YOLO.
Everything weve just done is basically fine. But theres one thing you should remember
and try to be careful with. Each one of the control-flow functions weve created so far
is basically a derived signal: it will rerun every time the value changes. In the examples
above, where the value switches from even to odd on every change, this is fine.
But consider the following example:
```rust
let (value, set_value) = create_signal(cx, 0);
let message = move || if value() > 5 {
"Big"
} else {
"Small"
};
view! { cx,
<p>{message}</p>
}
```
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"
} else {
log!("{}: rendering Small", value());
"Small"
};
```
As a user clicks a button, youd see something like this:
```
1: rendering Small
2: rendering Small
3: rendering Small
4: rendering Small
5: rendering Small
6: rendering Big
7: rendering Big
8: rendering Big
... ad infinitum
```
Every time `value` changes, it reruns the `if` statement. This makes sense, with
how reactivity works. But it has a downside. For a simple text node, rerunning
the `if` statement and rerendering isnt a big deal. But imagine it were
like this:
```rust
let message = move || if value() > 5 {
<Big/>
} else {
<Small/>
};
```
This rerenders `<Small/>` five times, then `<Big/>` infinitely. If theyre
loading resources, creating signals, or even just creating DOM nodes, this is
unnecessary work.
### `<Show/>`
The [`<Show/>`](https://docs.rs/leptos/latest/leptos/fn.Show.html) component is
the answer. You pass it a `when` condition function, a `fallback` to be shown if
the `when` function returns `false`, and children to be rendered if `when` is `true`.
```rust
let (value, set_value) = create_signal(cx, 0);
view! { cx,
<Show
when=move || value() > 5
fallback=|cx| view! { cx, <Small/> }
>
<Big/>
</Show>
}
```
`<Show/>` memoizes the `when` condition, so it only renders its `<Small/>` once,
continuing to show the same component until `value` is greater than five;
then it renders `<Big/>` once, continuing to show it indefinitely.
This is a helpful tool to avoid rerendering when using dynamic `if` expressions.
As always, there's some overhead: for a very simple node (like updating a single
text node, or updating a class or attribute), a `move || if ...` will be more
efficient. But if its at all expensive to render either branch, reach for
`<Show/>`.
## Note: Type Conversions
Theres one final thing its important to say in this section.
The `view` macro doesnt return the most-generic wrapping type
[`View`](https://docs.rs/leptos/latest/leptos/enum.View.html).
Instead, it returns things with types like `Fragment` or `HtmlElement<Input>`. This
can be a little annoying if youre returning different HTML elements from
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>
}
```
This strong typing is actually very powerful, because
[`HtmlElement`](https://docs.rs/leptos/0.1.3/leptos/struct.HtmlElement.html) is,
among other things, a smart pointer: each `HtmlElement<T>` type implements
`Deref` for the appropriate underlying `web_sys` type. In other words, in the browser
your `view` returns real DOM elements, and you can access native DOM methods on
them.
But it can be a little annoying in conditional logic like this, because you cant
return different types from different branches of a condition in Rust. There are two ways
to get yourself out of this situation:
1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>`
with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any)
2. If you have a variety of view types that are not all `HtmlElement`, convert them to
`View`s with [`.into_view(cx)`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
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>
}
```
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,113 @@
# Error Handling
[In the last chapter](./06_control_flow.md), we saw that you can render `Option<T>`:
in the `None` case, it will render nothing, and in the `T` case, it will render `T`
(that is, if `T` implements `IntoView`). You can actually do something very similar
with a `Result<T, E>`. In the `Err(_)` case, it will render nothing. In the `Ok(T)`
case, it will render the `T`.
Lets start with a simple component to capture a number input.
```rust
#[component]
fn NumericInput(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
// when input changes, try to parse a number from the input
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
<label>
"Type a number (or not!)"
<input type="number" on:input=on_input/>
<p>
"You entered "
<strong>{value}</strong>
</p>
</label>
}
}
```
Every time you change the input, `on_input` will attempt to parse its value into a 32-bit
integer (`i32`), and store it in our `value` signal, which is a `Result<i32, _>`. If you
type the number `42`, the UI will display
```
You entered 42
```
But if you type the string`foo`, it will display
```
You entered
```
This is not great. It saves us using `.unwrap_or_default()` or something, but it would be
much nicer if we could catch the error and do something with it.
You can do that, with the [`<ErrorBoundary/>`](https://docs.rs/leptos/latest/leptos/fn.ErrorBoundary.html)
component.
## `<ErrorBoundary/>`
An `<ErrorBoundary/>` is a little like the `<Show/>` component we saw in the last chapter.
If everythings okay—which is to say, if everything is `Ok(_)`—it renders its children.
But if theres an `Err(_)` rendered among those children, it will trigger the
`<ErrorBoundary/>`s `fallback`.
Lets add an `<ErrorBoundary/>` to this example.
```rust
#[component]
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>());
view! { cx,
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input=on_input/>
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors as strings, if we'd like
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
}
</ul>
</div>
}
>
<p>"You entered " <strong>{value}</strong></p>
</ErrorBoundary>
</label>
}
}
```
Now, if you type `42`, `value` is `Ok(42)` and youll see
```
You entered 42
```
If you type `foo`, value is `Err(_)` and the `fallback` will render. Weve chosen to render
the list of errors as a `String`, so youll see something like
```
Not a number! Errors:
- cannot parse integer from empty string
```
If you fix the error, the error message will disappear and the content youre wrapping in
an `<ErrorBoundary/>` will appear again.
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,288 @@
# Parent-Child Communication
You can think of your application as a nested tree of components. Each component
handles its own local state and manages a section of the user interface, so
components tend to be relatively self-contained.
Sometimes, though, youll want to communicate between a parent component and its
child. For example, imagine youve defined a `<FancyButton/>` component that adds
some styling, logging, or something else to a `<button/>`. You want to use a
`<FancyButton/>` in your `<App/>` component. But how can you communicate between
the two?
Its easy to communicate state from a parent component to a child component. We
covered some of this in the material on [components and props](./03_components.md).
Basically if you want the parent to communicate to the child, you can pass a
[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/struct.MaybeSignal.html) as a prop.
But what about the other direction? How can a child send notifications about events
or state changes back up to the parent?
There are four basic patterns of parent-child communication in Leptos.
## 1. Pass a [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html)
One approach is simply to pass a `WriteSignal` from the parent down to the child, and update
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/>
}
}
#[component]
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> impl IntoView {
view! { cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
"Toggle"
</button>
}
}
```
This pattern is simple, but you should be careful with it: passing around a `WriteSignal`
can make it hard to reason about your code. In this example, its pretty clear when you
read `<App/>` that you are handing off the ability to mutate `toggled`, but its not at
all clear when or how it will change. In this small, local example its easy to understand,
but if you find yourself passing around `WriteSignal`s like this throughout your code,
you should really consider whether this is making it too easy to write spaghetti code.
## 2. Use a Callback
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)/>
}
}
#[component]
pub fn ButtonB<F>(
cx: Scope,
on_click: F,
) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
{
view! { cx,
<button on:click=on_click>
"Toggle"
</button>
}
}
```
Youll notice that whereas `<ButtonA/>` was given a `WriteSignal` and decided how to mutate it,
`<ButtonB/>` simply fires an event: the mutation happens back in `<App/>`. This has the advantage
of keeping local state local, preventing the problem of spaghetti mutation. But it also means
the logic to mutate that signal needs to exist up in `<App/>`, not down in `<ButtonB/>`. These
are real trade-offs, not a simple right-or-wrong choice.
> Note the way we declare the generic type `F` here for the callback. If youre
> confused, look back at the [generic props](./03_components.html#generic-props) section
> of the chapter on components.
## 3. Use an Event Listener
You can actually write Option 2 in a slightly different way. If the callback maps directly onto
a native DOM event, you can add an `on:` listener directly to the place you use the component
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)/>
}
}
#[component]
pub fn ButtonC<F>(cx: Scope) -> impl IntoView {
view! { cx,
<button>"Toggle"</button>
}
}
```
This lets you write way less code in `<ButtonC/>` than you did for `<ButtonB/>`,
and still gives a correctly-typed event to the listener. This works by adding an
`on:` event listener to each element that `<ButtonC/>` returns: in this case, just
the one `<button>`.
Of course, this only works for actual DOM events that youre passing directly through
to the elements youre rendering in the component. For more complex logic that
doesnt map directly onto an element (say you create `<ValidatedForm/>` and want an
`on_valid_form_submit` callback) you should use Option 2.
## 4. Providing a Context
This version is actually a variant on Option 1. Say you have a deeply-nested component
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/>
}
}
#[component]
pub fn Layout(cx: Scope) -> impl IntoView {
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>
}
}
#[component]
pub fn ButtonD<F>(cx: Scope) -> impl IntoView {
todo!()
}
```
Now `<ButtonD/>` is no longer a direct child of `<App/>`, so you cant simply
pass your `WriteSignal` to its props. You could do whats sometimes called
“prop drilling,” adding a prop to each layer between the two:
```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/>
}
}
#[component]
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
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>
}
}
#[component]
pub fn ButtonD<F>(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
todo!()
}
```
This is a mess. `<Layout/>` and `<Content/>` dont need `set_toggled`; they just
pass it through to `<ButtonD/>`. But I need to declare the prop in triplicate.
This is not only annoying but hard to maintain: imagine we add a “half-toggled”
option and the type of `set_toggled` needs to change to an `enum`. We have to change
it in three places!
Isnt there some way to skip levels?
There is!
### The Context API
You can provide data that skips levels by using [`provide_context`](https://docs.rs/leptos/latest/leptos/fn.provide_context.html)
and [`use_context`](https://docs.rs/leptos/latest/leptos/fn.use_context.html). Contexts are identified
by the type of the data you provide (in this example, `WriteSignal<bool>`), and they exist in a top-down
tree that follows the contours of your UI tree. In this example, we can use context to skip the
unnecessary prop drilling.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
// share `set_toggled` with all children of this component
provide_context(cx, set_toggled);
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");
view! { cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
"Toggle"
</button>
}
}
```
The same caveats apply to this as to `<ButtonA/>`: passing a `WriteSignal`
around should be done with caution, as it allows you to mutate state from
arbitrary parts of your code. But when done carefully, this can be one of
the most effective techniques for global state management in Leptos: simply
provide the state at the highest level youll need it, and use it wherever
you need it lower down.
Note that there are no performance downsides to this approach. Because you
are passing a fine-grained reactive signal, _nothing happens_ in the intervening
components (`<Layout/>` and `<Content/>`) when you update it. You are communicating
directly between `<ButtonD/>` and `<App/>`. In fact—and this is the power of
fine-grained reactivity—you are communicating directly between a button click
in `<ButtonD/>` and a single text node in `<App/>`. Its as if the components
themselves dont exist at all. And, well... at runtime, they dont. Its just
signals and effects, all the way down.
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>

View File

@@ -0,0 +1,126 @@
# Component Children
Its pretty common to want to pass children into a component, just as you can pass
children into an HTML element. For example, imagine I have a `<FancyForm/>` component
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>
}
```
How can you do this in Leptos? There are basically two ways to pass components to
other components:
1. **render props**: properties that are functions that return a view
2. the **`children`** prop: a special component property that includes anything
you pass as a child to the component.
In fact, youve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
```rust
view! { cx,
<Show
// `when` is a normal prop
when=move || value() > 5
// `fallback` is a "render prop": a function that returns a view
fallback=|cx| view! { cx, <Small/> }
>
// `<Big/>` (and anything else here)
// will be given to the `children` prop
<Big/>
</Show>
}
```
Lets define a component that takes some children and a render prop.
```rust
#[component]
pub fn TakesChildren<F, IV>(
cx: Scope,
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,
/// `children` takes the `Children` type
children: Children,
) -> impl IntoView
where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
<h2>"Render Prop"</h2>
{render_prop()}
<h2>"Children"</h2>
{children(cx)}
}
}
```
`render_prop` and `children` are both functions, so we can call them to generate
the appropriate views. `children`, in particular, is an alias for
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
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>
}
```
## Manipulating Children
The [`Fragment`](https://docs.rs/leptos/latest/leptos/struct.Fragment.html) type is
basically a way of wrapping a `Vec<View>`. You can insert it anywhere into your view.
But you can also access those inner views directly to manipulate them. For example, heres
a component that takes its children and turns them into an unordered list.
```rust
#[component]
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
// Fragment has `nodes` field that contains a Vec<View>
let children = children(cx)
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect::<Vec<_>>();
view! { cx,
<ul>{children}</ul>
}
}
```
Calling it like this will create a list:
```rust
view! { cx,
<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

@@ -0,0 +1,5 @@
# Building User Interfaces
This first section will introduce you to the basic tools you need to build a reactive
user interface using Leptos. By the end of this section, you should be able to
build a simple, synchronous application that is rendered in the browser.

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

@@ -2,3 +2,8 @@
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

@@ -3,3 +3,5 @@
This example creates a simple counter in a client side rendered app with Rust and WASM!
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,35 +1,105 @@
use counter::*;
use leptos::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::*;
use web_sys::HtmlElement;
use counter::*;
#[wasm_bindgen_test]
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
);
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
// now let's click the `clear` button
clear.click();
// now let's test the <div> against the expected value
// we can do this by testing its `outerHTML`
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system, just to render the
// test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
);
// There's actually an easier way to do this...
// We can just test against a <SimpleCounter/> with the initial value 0
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
}
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> });
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> },
);
// You can do testing with vanilla DOM operations
let document = leptos::document();
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.dyn_into::<web_sys::HtmlElement>()
.unwrap();
let dec = clear
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.dyn_into::<web_sys::HtmlElement>()
.unwrap();
let text = dec
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.dyn_into::<web_sys::HtmlElement>()
.unwrap();
let inc = text
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.dyn_into::<web_sys::HtmlElement>()
.unwrap();
inc.click();
@@ -47,4 +117,40 @@ fn inc() {
clear.click();
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
// Or you can test against a sample view!
assert_eq!(
div.outer_html(),
run_scope(create_runtime(), |cx| {
let (value, _) = create_signal(cx, 0);
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
}
.outer_html())
);
inc.click();
assert_eq!(
div.outer_html(),
run_scope(create_runtime(), |cx| {
// because we've clicked, it's as if the signal is starting at 1
let (value, _) = create_signal(cx, 1);
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
}
.outer_html())
);
}

View File

@@ -8,11 +8,10 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
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,8 +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 = []
@@ -47,6 +47,7 @@ skip_feature_sets = [["ssr", "hydrate"]]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "counter_isomorphic"
# 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.
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
@@ -56,7 +57,7 @@ site-pkg-dir = "pkg"
# [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-address = "127.0.0.1:3000"
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.

View File

@@ -2,3 +2,8 @@
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

@@ -3,8 +3,8 @@
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
For this example the server must store the counter state since it can be modified by many users.
This means it is not possible to produce a working CSR-only version as a non-static server is required.
## 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)
@@ -28,7 +28,7 @@ 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.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. For examples with CSS you also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack

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)
@@ -194,16 +194,16 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
use futures::StreamExt;
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect_throw("couldn't connect to SSE stream");
.expect("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source.subscribe("message").unwrap().map(|value| {
value
.expect_throw("no message event")
.expect("no message event")
.1
.data()
.as_string()
.expect_throw("expected string value")
.expect("expected string value")
}),
);

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

@@ -34,9 +34,10 @@ cfg_if! {
crate::counters::register_server_functions();
// Setting this to None means we'll be using cargo-leptos and its env vars.
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_address.clone();
let addr = conf.leptos_options.site_addr.clone();
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
HttpServer::new(move || {
@@ -56,14 +57,11 @@ cfg_if! {
}
}
// client-only stuff for Trunk
// client-only main for Trunk
else {
use counter_isomorphic::counters::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counter/> });
// isomorphic counters cannot work in a Client-Side-Rendered only
// app as a server is required to maintain state
}
}
}

View File

@@ -0,0 +1,13 @@
[package]
name = "counter_without_macros"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
console_log = "0.2"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

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

View File

@@ -0,0 +1,5 @@
# Leptos Counter Example
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.

View File

@@ -2,6 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,48 @@
use leptos::{ev, html::*, *};
pub struct Props {
/// The starting value for the counter
pub initial_value: i32,
/// The change that should be applied each time the button is clicked.
pub step: i32,
}
/// A simple counter view.
pub fn view(cx: Scope, props: Props) -> impl IntoView {
let Props {
initial_value,
step,
} = props;
let (value, set_value) = create_signal(cx, initial_value);
div(cx)
.child((
cx,
button(cx)
.on(ev::click, move |_| set_value.update(|value| *value = 0))
.child((cx, "Clear")),
))
.child((
cx,
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value -= step)
})
.child((cx, "-1")),
))
.child((
cx,
span(cx)
.child((cx, "Value: "))
.child((cx, move || value.get()))
.child((cx, "!")),
))
.child((
cx,
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value += step)
})
.child((cx, "+1")),
))
}

View File

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

View File

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

View File

@@ -2,3 +2,8 @@
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,9 @@
# Leptos Counters Example
This example showcases a basic leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events.
## Client Side Rendering
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,5 +1,4 @@
use leptos::*;
use leptos::{For, ForProps};
use leptos::{For, ForProps, *};
const MANY_COUNTERS: usize = 1000;
@@ -66,9 +65,8 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<For
each=counters
key=|counter| counter.0
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! {
cx,
view=move |cx, (id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! { cx,
<Counter id value set_value/>
}
}
@@ -85,9 +83,11 @@ fn Counter(
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let input = move |ev| set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default());
let input = move |ev| {
set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
// just an example of how a cleanup function works
// this will run when the scope is disposed, i.e., when this row is deleted

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

@@ -0,0 +1,9 @@
# Leptos Counters Example on Rust Stable
This example showcases a basic Leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events. Unlike the other counters example, it will compile on Rust stable, because it has the `stable` feature enabled.
## Client Side Rendering
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

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/> })
@@ -72,7 +71,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view=move |cx, (id, (value, set_value))| {
view! {
cx,
<Counter id value set_value/>
@@ -91,9 +90,12 @@ fn Counter(
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let input = move |ev| set_value.set(event_target_value(&ev).parse::<i32>().unwrap_or_default());
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! { cx,
<li>

View File

@@ -0,0 +1,10 @@
[package]
name = "error_boundary"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
log = "0.4"
console_error_panic_hook = "0.1.7"

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,7 @@
# Leptos `<ErrorBoundary/>` Example
This example shows how to handle basic errors using Leptos.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,47 @@
use leptos::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
// when input changes, try to parse a number from the input
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input=on_input/>
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
// as strings, if we'd like
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
}
</ul>
</div>
}
>
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
<strong>{value}</strong>
</p>
</ErrorBoundary>
</label>
}
}

View File

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

View File

@@ -0,0 +1,91 @@
[package]
name = "errors_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
log = "0.4.17"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.0.0"
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" }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# 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
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,41 @@
# Leptos Errors Demonstration with Axum
This example demonstrates how Leptos Errors can work with an Axum backend on a server.
## Client Side Rendering
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send status codes.
## 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.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,59 @@
use crate::errors::AppError;
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<AppError> = errors
.get()
.into_iter()
.filter_map(|(_, v)| v.downcast_ref::<AppError>().cloned())
.collect();
log!("Errors: {errors:#?}");
// 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>{if errors.len() > 1 {"Errors"} else {"Error"}}</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, _)| *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 AppError {
#[error("Not Found")]
NotFound,
#[error("Internal Server Error")]
InternalServerError,
}
impl AppError {
pub fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound => StatusCode::NOT_FOUND,
AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -0,0 +1,45 @@
use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::landing::{App, AppProps};
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move |cx| view!{ cx, <App/> }
);
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}
}}

View File

@@ -0,0 +1,86 @@
use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
errors::AppError,
};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
_ = CauseInternalServerError::register();
}
#[server(CauseInternalServerError, "/api")]
pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
Err(ServerFnError::ServerError(
"Generic Server Error".to_string(),
))
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
//let id = use_context::<String>(cx);
provide_meta_context(cx);
view! {
cx,
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/errors_axum.css"/>
<Router fallback=|cx| {
let mut outside_errors = Errors::default();
outside_errors.insert_with_default_key(AppError::NotFound);
view! { cx,
<ErrorTemplate outside_errors/>
}
.into_view(cx)
}>
<header>
<h1>"Error Examples:"</h1>
</header>
<main>
<Routes>
<Route path="" view=|cx| view! {
cx,
<ExampleErrors/>
}/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
view! { cx,
<p>
"These links will load 404 pages since they do not exist. Verify with browser development tools: " <br/>
<a href="/404">"This links to a page that does not exist"</a><br/>
<a href="/404" target="_blank">"Same link, but in a new tab"</a>
</p>
<p>
"After pressing this button check browser network tools. Can be used even when WASM is blocked:"
<ActionForm action=generate_internal_error>
<input name="error1" type="submit" value="Generate Internal Server Error"/>
</ActionForm>
</p>
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
<div>
// note that the error boundries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribue to the
// final status code sent by the server when producing ssr pages.
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ReturnsError/>
</ErrorBoundary>
</div>
}
}
#[component]
pub fn ReturnsError(_cx: Scope) -> impl IntoView {
Err::<String, AppError>(AppError::InternalServerError)
}

View File

@@ -0,0 +1,24 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
pub mod landing;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::landing::*;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -0,0 +1,72 @@
use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use crate::landing::*;
use axum::body::Body as AxumBody;
use axum::{
extract::{Extension, Path},
http::Request,
response::{IntoResponse, Response},
routing::{get, post},
Router,
};
use errors_axum::*;
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
}}
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
Extension(options): Extension<Arc<LeptosOptions>>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
(*options).clone(),
move |cx| {
provide_context(cx, id.clone());
},
|cx| view! { cx, <App/> },
);
handler(req).await.into_response()
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
crate::landing::register_server_functions();
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// this is if we were using client-only rending with Trunk
#[cfg(not(feature = "ssr"))]
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// The server is needed to demonstrate the error statuses.
}

View File

@@ -0,0 +1,3 @@
.pending {
color: purple;
}

View File

@@ -2,3 +2,8 @@
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"

9
examples/fetch/README.md Normal file
View File

@@ -0,0 +1,9 @@
# Client Side Fetch
This example shows how to fetch data from the client in WebAssembly.
## Client Side Rendering
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -9,6 +9,12 @@
max-width: 250px;
height: auto;
}
.error {
border: 1px solid red;
color: red;
background-color: lightpink;
}
</style>
<body></body>
</html>

View File

@@ -1,3 +1,4 @@
use anyhow::Result;
use leptos::*;
use serde::{Deserialize, Serialize};
@@ -6,18 +7,18 @@ pub struct Cat {
url: String,
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
if count > 0 {
// make the request
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={}",
count
"https://api.thecatapi.com/v1/images/search?limit={count}",
))
.send()
.await
.map_err(|_| ())?
.await?
// convert it to JSON
.json::<Vec<Cat>>()
.await
.map_err(|_| ())?
.await?
// extract the URL field for each cat
.into_iter()
.map(|cat| cat.url)
.collect::<Vec<_>>();
@@ -29,9 +30,45 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
pub fn fetch_example(cx: Scope) -> impl IntoView {
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
view! { cx,
// we use local_resource here because
// 1) anyhow::Result isn't serializable/deserializable
// 2) we're not doing server-side rendering in this example anyway
// (during SSR, create_resource will begin loading on the server and resolve on the client)
let cats = create_local_resource(cx, cat_count, fetch_cats);
let fallback = move |cx, errors: RwSignal<Errors>| {
let error_list = move || {
errors.with(|errors| {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
})
};
view! { cx,
<div class="error">
<h2>"Error"</h2>
<ul>{error_list}</ul>
</div>
}
};
// the renderer can handle Option<_> and Result<_> states
// by displaying nothing for None if the resource is still loading
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just implement our happy path and let the framework handle the rest
let cats_view = move || {
cats.with(cx, |data| {
data.iter()
.flatten()
.map(|cat| view! { cx, <img src={cat}/> })
.collect::<Vec<_>>()
})
};
view! { cx,
<div>
<label>
"How many cats would you like?"
@@ -43,25 +80,11 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
}
/>
</label>
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
{move || {
cats.read().map(|data| match data {
Err(_) => view! { cx, <pre>"Error"</pre> }.into_view(cx),
Ok(cats) => view! { cx,
<div>{
cats.iter()
.map(|src| {
view! { cx,
<img src={src}/>
}
})
.collect::<Vec<_>>()
}</div>
}.into_view(cx),
})
}
}
</Transition>
<ErrorBoundary fallback>
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
{cats_view}
</Transition>
</ErrorBoundary>
</div>
}
}

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