Compare commits

..

262 Commits

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
143 changed files with 1272 additions and 3805 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.2.5"
version = "0.2.4"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.2.5" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.5" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.5" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.5" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.5" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.5" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.5" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.5" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
leptos_router = { path = "./router", version = "0.2.5" }
leptos_meta = { path = "./meta", default-features = false, version = "0.2.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
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

@@ -74,9 +74,3 @@ dependencies = ["test-all"]
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
[env]
RUSTFLAGS=""
[env.github-actions]
RUSTFLAGS="-D warnings"

View File

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

View File

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

View File

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

View File

@@ -162,77 +162,6 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
runtime.dispose();
}
#[bench]
fn rs_deep_update(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let sc = ClientRuntime::new_root_scope();
b.iter(|| {
let signal = signal!(sc, 0);
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(signal!(sc, move || prev.get() + 1))
} else {
memos.push(signal!(sc, move || signal.get() + 1))
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
});
}
#[bench]
fn rs_fanning_out(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let sig = signal!(cx, 0);
let memos = (0..1000)
.map(|_| signal!(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
});
}
#[bench]
fn rs_narrowing_update(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let memo = signal!(cx, {
let sigs = sigs.clone();
move || {
sigs.iter().map(|r| r.get()).sum::<i32>()
}
});
assert_eq!(memo.get(), 499500);
signal!(cx, {
let acc = Rc::clone(&acc);
move || {
acc.set(memo.get());
}
});
assert_eq!(acc.get(), 499500);
sigs[1].update(|n| *n += 1);
sigs[10].update(|n| *n += 1);
sigs[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo.get(), 499503);
});
}
#[bench]
fn l021_deep_creation(b: &mut Bencher) {
use l021::*;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -84,7 +84,7 @@ fn FancyMath(cx: Scope) -> impl IntoView {
This kind of “provide a signal in a parent, consume it in a child” should be familiar
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
pattern you use to communicate between parents and children works for grandparents and
grandchildren, or any ancestors and descendants: in other words, between “global” state
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,
@@ -122,7 +122,6 @@ fn App(cx: Scope) -> impl IntoView {
provide_context(cx, state);
// ...
}
```
Then child components can access “slices” of that state with fine-grained
@@ -169,6 +168,4 @@ 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.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh">
<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

@@ -26,7 +26,7 @@
- [Nested Routing](./router/17_nested_routing.md)
- [Params and Queries](./router/18_params_and_queries.md)
- [`<A/>`](./router/19_a.md)
- [`<Form/>`](./router/20_form.md)
- [`<Form/>`]()
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Metadata]()
- [SSR]()

View File

@@ -50,6 +50,4 @@ 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.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<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

@@ -69,6 +69,4 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
This inversion of the flow of control makes it easier to add or remove individual resources, as you dont need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which well talk about during a later chapter.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>

View File

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

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

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

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

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

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

View File

@@ -16,7 +16,7 @@ The Leptos Router works with the path and query (`/blog/search?q=Search`). Given
## The Philosophy
In most cases, the path should drive what is displayed on the page. From the users perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
In 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.

View File

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

View File

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

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

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

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

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

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

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

@@ -285,6 +285,4 @@ 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.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<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

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

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

View File

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

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
broadcaster = "1"
console_log = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"

View File

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

View File

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

View File

@@ -1,44 +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.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
pub fn view(cx: Scope, props: Props) -> impl IntoView {
let Props {
initial_value,
step,
} = props;
let (value, set_value) = create_signal(cx, initial_value);
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
div(cx)
// children can be added with .child()
// this takes any type that implements IntoView as its argument
// for example, a string or an HtmlElement<_>
.child(
.child((
cx,
button(cx)
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_value.update(|value| *value = 0))
.child("Clear"),
)
.child(
.child((cx, "Clear")),
))
.child((
cx,
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value -= step)
})
.child("-1"),
)
.child(
.child((cx, "-1")),
))
.child((
cx,
span(cx)
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child((cx, "Value: "))
.child((cx, move || value.get()))
.child("!"),
)
.child(
.child((cx, "!")),
))
.child((
cx,
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value += step)
})
.child("+1"),
)
.child((cx, "+1")),
))
}

View File

@@ -1,8 +1,16 @@
use counter_without_macros::counter;
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(cx, 0, 1))
mount_to_body(|cx| {
counter::view(
cx,
counter::Props {
initial_value: 0,
step: 1,
},
)
})
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0.0"
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 = [

View File

@@ -70,8 +70,8 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
</p>
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
<div>
// note that the error boundaries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribute to the
// 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/>

View File

@@ -9,7 +9,7 @@ leptos = { path = "../../leptos" }
reqwasm = "0.5.0"
serde = { version = "1", features = ["derive"] }
log = "0.4"
console_log = "1"
console_log = "0.2"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

@@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

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

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0.0"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
console_log = "1"
console_log = "0.2"
log = "0.4"
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router", features = ["csr"] }

View File

@@ -28,7 +28,19 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
</nav>
<main>
<Routes>
<ContactRoutes/>
<Route
path=""
view=move |cx| view! { cx, <ContactList/> }
>
<Route
path=":id"
view=move |cx| view! { cx, <Contact/> }
/>
<Route
path="/"
view=move |_| view! { cx, <p>"Select a contact."</p> }
/>
</Route>
<Route
path="about"
view=move |cx| view! { cx, <About/> }
@@ -47,27 +59,6 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
}
}
// You can define other routes in their own component.
// Use a #[component(transparent)] that returns a <Route/>.
#[component(transparent)]
pub fn ContactRoutes(cx: Scope) -> impl IntoView {
view! { cx,
<Route
path=""
view=move |cx| view! { cx, <ContactList/> }
>
<Route
path=":id"
view=move |cx| view! { cx, <Contact/> }
/>
<Route
path="/"
view=move |_| view! { cx, <p>"Select a contact."</p> }
/>
</Route>
}
}
#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
log::debug!("rendering <ContactList/>");

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "1.0.0"
console_log = "0.2.0"
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1.7"
futures = "0.3.25"

View File

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

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "1"
console_log = "0.2"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1"
console_log = "1"
console_log = "0.2"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", default-features = false }
log = "0.4"
console_log = "1"
console_log = "0.2"
console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }

View File

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

View File

@@ -18,10 +18,9 @@ use http::StatusCode;
use leptos::{
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
*,
};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
@@ -95,7 +94,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -151,9 +150,9 @@ pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|_cx| {})
}
/// An Actix [Route](actix_web::Route) that listens for `GET` or `POST` requests with
/// Leptos server function arguments in the URL (`GET`) or body (`POST`),
/// runs the server function if found, and returns the resulting [HttpResponse].
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
/// Leptos server function arguments in the body, runs the server function if found,
/// and returns the resulting [HttpResponse].
///
/// This provides the [HttpRequest] to the server [Scope](leptos::Scope).
///
@@ -169,7 +168,7 @@ pub fn handle_server_fns() -> Route {
pub fn handle_server_fns_with_context(
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> Route {
web::to(
web::post().to(
move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| {
let additional_context = additional_context.clone();
async move {
@@ -195,13 +194,7 @@ pub fn handle_server_fns_with_context(
provide_context(cx, req.clone());
provide_context(cx, res_options.clone());
let query = req.query_string().as_bytes();
let data = match &server_fn.encoding {
Encoding::Url | Encoding::Cbor => body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
match (server_fn.trait_obj)(cx, data).await {
match server_fn(cx, body).await {
Ok(serialized) => {
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
@@ -264,10 +257,8 @@ pub fn handle_server_fns_with_context(
}
}
}
Err(e) => HttpResponse::InternalServerError().body(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
),
Err(e) => HttpResponse::InternalServerError()
.body(e.to_string()),
}
} else {
HttpResponse::BadRequest().body(format!(
@@ -349,15 +340,14 @@ where
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using
/// [render_to_stream_in_order](leptos::ssr::render_to_stream_in_order),
/// and includes everything described in the documentation for that function.
/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -419,8 +409,8 @@ where
/// The provides a [MetaContext] and a [RouterIntegrationContext] to the apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_string_async](leptos::ssr::render_to_string_async), and
/// includes everything described in the documentation for that function.
/// The HTML stream is rendered using [render_to_string_async], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -738,7 +728,7 @@ async fn stream_app(
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| generate_head_metadata_separated(cx).1.into(),
move |cx| generate_head_metadata(cx).into(),
additional_context,
);
@@ -755,7 +745,7 @@ async fn stream_app_in_order(
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move |cx| {
generate_head_metadata_separated(cx).1.into()
generate_head_metadata(cx).into()
},
additional_context,
);
@@ -772,7 +762,7 @@ async fn build_stream_response(
) -> HttpResponse {
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
html_parts(options, use_context::<MetaContext>(cx).as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,12 +14,11 @@ use http::{header, method::Method, uri::Uri, version::Version, StatusCode};
use hyper::body;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
ssr::*,
*,
};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::{generate_head_metadata, MetaContext};
use leptos_router::*;
use parking_lot::RwLock;
use std::{pin::Pin, sync::Arc};
@@ -91,7 +90,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -186,7 +185,6 @@ async fn handle_server_fns_inner(
) -> Result<Response> {
let fn_name = req.params::<String>()?;
let headers = req.headers().clone();
let query = req.query_string().unwrap_or("").to_owned().into();
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking({
move || {
@@ -209,14 +207,7 @@ async fn handle_server_fns_inner(
// Add this so that we can set headers and status of the response
provide_context(cx, ResponseOptions::default());
let data = match &server_fn.encoding {
Encoding::Url | Encoding::Cbor => {
&req_parts.body
}
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
match (server_fn.trait_obj)(cx, data).await {
match server_fn(cx, &req_parts.body).await {
Ok(serialized) => {
// If ResponseOptions are set, add the headers and status to the request
let res_options =
@@ -301,10 +292,7 @@ async fn handle_server_fns_inner(
}
Err(e) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
)),
.body(Body::from(e.to_string())),
}
} else {
Response::builder()
@@ -397,7 +385,7 @@ where
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
@@ -548,7 +536,7 @@ where
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
|cx| generate_head_metadata(cx).into(),
add_context,
);
@@ -605,7 +593,7 @@ async fn forward_stream(
) {
let cx = Scope { runtime, id: scope };
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
html_parts(options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
@@ -629,7 +617,7 @@ async fn forward_stream(
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network
@@ -712,7 +700,7 @@ where
let (bundle, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
|cx| generate_head_metadata(cx).into(),
add_context,
);

View File

@@ -56,7 +56,7 @@
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite),
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and
//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz)
@@ -218,21 +218,3 @@ pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
/// }
/// ```
pub type AttributeValue = Box<dyn IntoAttribute>;
#[doc(hidden)]
pub trait Component<P> {}
#[doc(hidden)]
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}
impl<P, F, R> Component<P> for F where F: FnOnce(::leptos::Scope, P) -> R {}
#[doc(hidden)]
pub fn component_props_builder<P: Props>(
_f: &impl Component<P>,
) -> <P as Props>::Builder {
<P as Props>::builder()
}

View File

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

View File

@@ -79,9 +79,9 @@ where
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
if context.ready() {
Fragment::lazy(Box::new(|| vec![orig_child(cx).into_view(cx)])).into_view(cx)
orig_child(cx).into_view(cx)
} else {
Fragment::lazy(Box::new(|| vec![fallback().into_view(cx)])).into_view(cx)
fallback().into_view(cx)
}
} else {
use leptos_reactive::signal_prelude::*;
@@ -108,12 +108,10 @@ where
let orig_child = Rc::clone(&orig_child);
move || {
HydrationCtx::continue_from(current_id.clone());
Fragment::lazy(Box::new(move || {
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
}))
.into_view(cx)
.render_to_string(cx)
.to_string()
DynChild::new(move || orig_child(cx))
.into_view(cx)
.render_to_string(cx)
.to_string()
}
},
// in-order streaming
@@ -121,16 +119,14 @@ where
let current_id = current_id.clone();
move || {
HydrationCtx::continue_from(current_id.clone());
Fragment::lazy(Box::new(move || {
vec![DynChild::new(move || orig_child(cx)).into_view(cx)]
}))
.into_view(cx)
.into_stream_chunks(cx)
DynChild::new(move || orig_child(cx))
.into_view(cx)
.into_stream_chunks(cx)
}
},
}
);
// return the fallback for now, wrapped in fragment identifier
// return the fallback for now, wrapped in fragment identifer
fallback().into_view(cx)
}
};

View File

@@ -1,13 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
pkg
# These are backup files generated by rustfmt
**/*.rs.bk
# node e2e test tools and outputs
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/

View File

@@ -1,78 +0,0 @@
[package]
name = "leptos_start"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "1"
cfg-if = "1"
leptos = { path = "../../..", default-features = false, features = ["serde"] }
leptos_actix = { path = "../../../../integrations/actix", optional = true }
leptos_router = { path = "../../../../router", default-features = false }
log = "0.4"
simple_logger = "4"
wasm-bindgen = "0.2"
serde = "1.0.159"
tokio = { version = "1.27.0", features = ["time"], optional = true }
[features]
hydrate = ["leptos/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_router/ssr",
"dep:tokio",
]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos_start"
# 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"
# 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.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that 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
[workspace]

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 henrik
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

@@ -1,61 +0,0 @@
<picture>
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
</picture>
# Leptos Starter Template
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
## Creating your template repo
If you don't have `cargo-leptos` installed you can install it with
`cargo install cargo-leptos`
Then run
`cargo leptos new --git leptos-rs/start`
to generate a new project template.
`cd {projectname}`
to go to your newly created project.
Of course you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
## Running your project
`cargo leptos watch`
## Installing Additional Tools
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
5. `npm install -g sass` - install `dart-sass` (should be optional in future)
## Executing a Server on a Remote Machine Without the Toolchain
After running a `cargo leptos build --release` the minimum files needed are:
1. The server binary located in `target/server/release`
2. The `site` directory and all files within located in `target/site`
Copy these files to your remote server. The directory structure should be:
```text
leptos_start
site/
```
Set the following enviornment variables (updating for your project as needed):
```text
LEPTOS_OUTPUT_NAME="leptos_start"
LEPTOS_SITE_ROOT="site"
LEPTOS_SITE_PKG_DIR="pkg"
LEPTOS_SITE_ADDR="127.0.0.1:3000"
LEPTOS_RELOAD_PORT="3001"
```
Finally, run the server binary.

View File

@@ -1,74 +0,0 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
},
"node_modules/@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.28.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"node_modules/playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.28.0"
}
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true
}
}
}

View File

@@ -1,13 +0,0 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
}

View File

@@ -1,107 +0,0 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@@ -1,9 +0,0 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

View File

@@ -1,219 +0,0 @@
use leptos::*;
use leptos_router::*;
#[server(OneSecondFn "/api")]
async fn one_second_fn(query: ()) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
Ok(())
}
#[server(TwoSecondFn "/api")]
async fn two_second_fn(query: ()) -> Result<(), ServerFnError> {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
Ok(())
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let style = r#"
nav {
display: flex;
width: 100%;
justify-content: space-around;
}
[aria-current] {
font-weight: bold;
}
"#;
view! {
cx,
<style>{style}</style>
<Router>
<nav>
<A href="/out-of-order">"Out-of-Order"</A>
<A href="/in-order">"In-Order"</A>
<A href="/async">"Async"</A>
</nav>
<main>
<Routes>
<Route
path=""
view=|cx| view! { cx, <Redirect path="/out-of-order"/> }
/>
// out-of-order
<Route
path="out-of-order"
view=|cx| view! { cx,
<SecondaryNav/>
<h1>"Out-of-Order"</h1>
<Outlet/>
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
</Route>
// in-order
<Route
path="in-order"
ssr=SsrMode::InOrder
view=|cx| view! { cx,
<SecondaryNav/>
<h1>"In-Order"</h1>
<Outlet/>
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
</Route>
// async
<Route
path="async"
ssr=SsrMode::Async
view=|cx| view! { cx,
<SecondaryNav/>
<h1>"Async"</h1>
<Outlet/>
}
>
<Route path="" view=|cx| view! { cx, <Nested/> }/>
<Route path="single" view=|cx| view! { cx, <Single/> }/>
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn SecondaryNav(cx: Scope) -> impl IntoView {
view! { cx,
<nav>
<A href="" exact=true>"Nested"</A>
<A href="single">"Single"</A>
<A href="parallel">"Parallel"</A>
<A href="inside-component">"Inside Component"</A>
</nav>
}
}
#[component]
fn Nested(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
let two_second = create_resource(cx, || (), two_second_fn);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(|_| "Loaded 1!")
}}
<br/><br/>
<Suspense fallback=|| "Loading 2...">
"Two Second: "
{move || {
two_second.read(cx).map(|_| "Loaded 2!")
}}
</Suspense>
</Suspense>
</div>
}
}
#[component]
fn Parallel(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
let two_second = create_resource(cx, || (), two_second_fn);
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(move |_| view! { cx,
"Loaded 1"
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
})
}}
</Suspense>
<br/><br/>
<Suspense fallback=|| "Loading 2...">
"Two Second: "
{move || {
two_second.read(cx).map(move |_| view! { cx,
"Loaded 2"
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
})
}}
</Suspense>
</div>
}
}
#[component]
fn Single(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(|_| "Loaded 1!")
}}
</Suspense>
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
<div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</div>
</div>
}
}
#[component]
fn InsideComponent(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div>
<p><code>"<Suspense/>"</code> " inside another component should work."</p>
<InsideComponentChild/>
<p>"Children following " <code>"<Suspense/>"</code> " should hydrate properly."</p>
<div>
<button on:click=move |_| set_count.update(|n| *n += 1)>
{count}
</button>
</div>
</div>
}
}
#[component]
fn InsideComponentChild(cx: Scope) -> impl IntoView {
let one_second = create_resource(cx, || (), one_second_fn);
view! { cx,
<Suspense fallback=|| "Loading 1...">
"One Second: "
{move || {
one_second.read(cx).map(|_| "Loaded 1!")
}}
</Suspense>
}
}

View File

@@ -1,23 +0,0 @@
pub mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
use app::*;
use leptos::*;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move |cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -1,42 +0,0 @@
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use actix_files::Files;
use actix_web::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
use leptos_start::app::*;
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
OneSecondFn::register().unwrap();
TwoSecondFn::register().unwrap();
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
|cx| view! { cx, <App/> },
)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(addr)?
.run()
.await
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -10,6 +10,7 @@ readme = "../README.md"
[dependencies]
config = "0.13.3"
fs = "0.0.5"
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"

View File

@@ -34,11 +34,6 @@ leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3"
features = [
"DocumentFragment",
"Element",
"HtmlTemplateElement",
"NodeList",
"Window",
"console",
"Comment",
"Document",
@@ -50,7 +45,6 @@ features = [
"TreeWalker",
# Events we cast to in leptos_macro -- added here so we don't force users to import them
"AddEventListenerOptions",
"AnimationEvent",
"BeforeUnloadEvent",
"ClipboardEvent",

View File

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

View File

@@ -28,15 +28,9 @@ pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
event.event_delegation_key(),
event_name,
event_handler,
&None,
);
} else {
add_event_listener_undelegated(
target,
&event_name,
event_handler,
&None,
);
add_event_listener_undelegated(target, &event_name, event_handler);
}
}
@@ -49,7 +43,6 @@ pub fn add_event_listener<E>(
event_name: Cow<'static, str>,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
options: &Option<web_sys::AddEventListenerOptions>,
) where
E: FromWasmAbi + 'static,
{
@@ -57,10 +50,8 @@ pub fn add_event_listener<E>(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -68,7 +59,7 @@ pub fn add_event_listener<E>(
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let key = intern(&key);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
add_delegated_event_listener(&key, event_name, options);
add_delegated_event_listener(&key, event_name);
}
#[doc(hidden)]
@@ -78,35 +69,22 @@ pub(crate) fn add_event_listener_undelegated<E>(
event_name: &str,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
options: &Option<web_sys::AddEventListenerOptions>,
) where
E: FromWasmAbi + 'static,
{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
leptos_reactive::SpecialNonReactiveZone::enter();
let span = ::tracing::Span::current();
let cb = move |e| {
let _guard = span.enter();
cb(e);
};
leptos_reactive::SpecialNonReactiveZone::exit();
}
}
let event_name = intern(event_name);
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
if let Some(options) = options {
_ = target
.add_event_listener_with_callback_and_add_event_listener_options(
event_name,
cb.unchecked_ref(),
options,
);
} else {
_ = target
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
// cf eventHandler in ryansolid/dom-expressions
@@ -114,7 +92,6 @@ pub(crate) fn add_event_listener_undelegated<E>(
pub(crate) fn add_delegated_event_listener(
key: &str,
event_name: Cow<'static, str>,
options: &Option<web_sys::AddEventListenerOptions>,
) {
GLOBAL_EVENTS.with(|global_events| {
let mut events = global_events.borrow_mut();
@@ -186,19 +163,10 @@ pub(crate) fn add_delegated_event_listener(
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
let handler = Closure::wrap(handler).into_js_value();
if let Some(options) = options {
_ = crate::window().add_event_listener_with_callback_and_add_event_listener_options(
&event_name,
handler.unchecked_ref(),
options,
);
} else {
_ = crate::window().add_event_listener_with_callback(
&event_name,
handler.unchecked_ref(),
);
}
_ = crate::window().add_event_listener_with_callback(
&event_name,
handler.unchecked_ref(),
);
// register that we've created handler
events.insert(event_name);

View File

@@ -22,12 +22,6 @@ pub trait EventDescriptor: Clone {
fn bubbles(&self) -> bool {
true
}
/// Return the options for this type. This is only used when you create a [`Custom`] event
/// handler.
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
&None
}
}
/// Overrides the [`EventDescriptor::bubbles`] method to always return
@@ -55,7 +49,6 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
/// A custom event.
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
name: Cow<'static, str>,
options: Option<web_sys::AddEventListenerOptions>,
_event_type: PhantomData<E>,
}
@@ -63,7 +56,6 @@ impl<E: FromWasmAbi> Clone for Custom<E> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
options: self.options.clone(),
_event_type: PhantomData,
}
}
@@ -83,10 +75,6 @@ impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
fn bubbles(&self) -> bool {
false
}
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
&self.options
}
}
impl<E: FromWasmAbi> Custom<E> {
@@ -96,35 +84,9 @@ impl<E: FromWasmAbi> Custom<E> {
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
options: None,
_event_type: PhantomData,
}
}
/// Modify the [`AddEventListenerOptions`] used for this event listener.
///
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # let canvas_ref: NodeRef<html::Canvas> = create_node_ref(cx);
/// let mut non_passive_wheel = ev::Custom::<ev::WheelEvent>::new("wheel");
/// # if false {
/// let options = non_passive_wheel.options_mut();
/// options.passive(false);
/// # }
/// canvas_ref.on_load(cx, move |canvas: HtmlElement<html::Canvas>| {
/// canvas.on(non_passive_wheel, move |_event| {
/// // Handle _event
/// });
/// });
/// # });
/// ```
///
/// [`AddEventListenerOptions`]: web_sys::AddEventListenerOptions
pub fn options_mut(&mut self) -> &mut web_sys::AddEventListenerOptions {
self.options
.get_or_insert_with(web_sys::AddEventListenerOptions::new)
}
}
macro_rules! generate_event_types {

View File

@@ -203,10 +203,8 @@ pub fn set_timeout_with_handle(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -243,7 +241,7 @@ pub fn set_timeout_with_handle(
pub fn debounce<T: 'static>(
cx: Scope,
delay: Duration,
#[allow(unused_mut)] mut cb: impl FnMut(T) + 'static,
mut cb: impl FnMut(T) + 'static,
) -> impl FnMut(T) {
use std::{
cell::{Cell, RefCell},
@@ -254,10 +252,8 @@ pub fn debounce<T: 'static>(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |value| {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(value);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -323,10 +319,8 @@ pub fn set_interval(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -355,10 +349,8 @@ pub fn set_interval_with_handle(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
@@ -385,10 +377,8 @@ pub fn window_event_listener(
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}

View File

@@ -74,13 +74,13 @@ pub trait ElementDescriptor: ElementDescriptorBounds {
/// The name of the element, i.e., `div`, `p`, `custom-element`.
fn name(&self) -> Cow<'static, str>;
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
/// Determains if the tag is void, i.e., `<input>` and `<br>`.
fn is_void(&self) -> bool {
false
}
/// A unique `id` that should be generated for each new instance of
/// this element, and be consistent for both SSR and CSR.
/// this element, and be consistant for both SSR and CSR.
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> &HydrationKey;
}
@@ -573,23 +573,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
self
}
/// Checks to see if this element is mounted to the DOM as a child
/// of `body`.
///
/// This method will always return [`None`] on non-wasm CSR targets.
pub fn is_mounted(&self) -> bool {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
crate::document()
.body()
.unwrap()
.contains(Some(self.element.as_ref()))
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
false
}
/// Adds an attribute to this element.
#[track_caller]
pub fn attr(
@@ -696,104 +679,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
this
}
/// Sets the class on the element as the class signal changes.
#[track_caller]
pub fn dyn_classes<I, C>(
self,
classes_signal: impl Fn() -> I + 'static,
) -> Self
where
I: IntoIterator<Item = C>,
C: Into<Cow<'static, str>>,
{
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
use smallvec::SmallVec;
let class_list = self.element.as_ref().class_list();
leptos_reactive::create_effect(
self.cx,
move |prev_classes: Option<
SmallVec<[Cow<'static, str>; 4]>,
>| {
let classes = classes_signal()
.into_iter()
.map(Into::into)
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
);
let mut new_classes = classes
.iter()
.flat_map(|classes| classes.split_whitespace());
if let Some(prev_classes) = prev_classes {
let mut old_classes = prev_classes
.iter()
.flat_map(|classes| classes.split_whitespace());
// Remove old classes
for prev_class in old_classes.clone() {
if !new_classes.any(|c| c == prev_class) {
class_list.remove_1(prev_class).unwrap_or_else(
|err| {
panic!(
"failed to add class \
`{prev_class}`, error: {err:#?}"
)
},
);
}
}
// Add new classes
for class in new_classes {
if !old_classes.any(|c| c == class) {
class_list.add_1(class).unwrap_or_else(|err| {
panic!(
"failed to remove class `{class}`, \
error: {err:#?}"
)
});
}
}
} else {
let new_classes = new_classes
.map(ToOwned::to_owned)
.collect::<SmallVec<[_; 4]>>();
for class in &new_classes {
class_list.add_1(class).unwrap_or_else(|err| {
panic!(
"failed to add class `{class}`, error: \
{err:#?}"
)
});
}
}
classes
},
);
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
classes_signal()
.into_iter()
.map(Into::into)
.flat_map(|classes| {
classes
.split_whitespace()
.map(ToString::to_string)
.collect::<SmallVec<[_; 4]>>()
})
.fold(self, |this, class| this.class(class, true))
}
}
/// Sets a property on an element.
#[track_caller]
pub fn prop(
@@ -849,14 +734,12 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
key,
event_name,
event_handler,
event.options(),
);
} else {
add_event_listener_undelegated(
self.element.as_ref(),
&event_name,
event_handler,
event.options(),
);
}

View File

@@ -681,13 +681,12 @@ impl View {
match &self {
Self::Element(el) => {
if event.bubbles() {
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler, &None);
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler);
} else {
add_event_listener_undelegated(
&el.element,
&event.name(),
event_handler,
&None,
);
}
}

View File

@@ -91,10 +91,15 @@ pub(crate) fn property_helper(
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |_| {
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
property_expression(&el, prop_name, new.clone());
if old.as_ref() != Some(&new)
&& !(old.is_none()
&& new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
}

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