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
65 changed files with 2230 additions and 199 deletions

View File

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

@@ -52,6 +52,7 @@ dependencies = [
{ name = "check", path = "examples/ssr_modes" },
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/tailwind_csr_trunk" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todo_app_sqlite_viz" },

View File

@@ -78,7 +78,7 @@ rustup target add wasm32-unknown-unknown
If youre on `stable`, note the following:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0", features = ["stable"] }`
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`,
youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)

View File

@@ -60,7 +60,7 @@ Your directory structure should now look something like this
leptos_tutorial
├── src
│ └── main.rs
├── Cargo.html
├── Cargo.toml
├── index.html
```

View File

@@ -21,10 +21,11 @@
- [Actions](./async/13_actions.md)
- [Responding to Changes with `create_effect`](./14_create_effect.md)
- [Global State Management](./15_global_state.md)
- [Router]()
- [Fundamentals]()
- [defining `<Routes/>`]()
- [`<A/>`]()
- [Router](./router/README.md)
- [Defining `<Routes/>`](./router/16_routes.md)
- [Nested Routing](./router/17_nested_routing.md)
- [Params and Queries](./router/18_params_and_queries.md)
- [`<A/>`](./router/19_a.md)
- [`<Form/>`]()
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Metadata]()
@@ -33,6 +34,9 @@
- [`cargo-leptos`]()
- [Hydration Footguns]()
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
- [Headers]()
- [Cookies]()
- [Server Functions]()
@@ -41,3 +45,4 @@
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size]()

View File

@@ -69,4 +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.
<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>
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
# Routing
## The Basics
Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?”
A URL consists of many parts. For example, the URL `https://leptos.dev/blog/search?q=Search#results` consists of
- a _scheme_: `https`
- a _domain_: `leptos.dev`
- a **path**: `/blog/search`
- a **query** (or **search**): `?q=Search`
- a _hash_: `#results`
The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page?
## The Philosophy
In most cases, the path should drive what is displayed on the page. From the users perspective, for most appliations, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
The router handles most of this work for you by mapping the current location to particular components.

View File

@@ -30,7 +30,7 @@ Now lets say Id like to update the list of CSS classes on this element dyn
For example, lets say I want to add the class `red` when the count is odd. I can
do this using the `class:` syntax.
```rust
class:red=move || count() & 1 == 1
class:red=move || count() % 2 == 1
```
`class:` attributes take
1. the class name, following the colon (`red`)

View File

@@ -238,7 +238,7 @@ which allows you to easily pass props with different values.
In this case, its helpful to know about the
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
is a enumerated type that represents any kind of readable reactive signal. It can
is an enumerated type that represents any kind of readable reactive signal. It can
be useful when defining APIs for components youll want to reuse while passing
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
reactive value.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,14 +9,14 @@ cfg_if! {
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("hydrate mode - hydrating");
log!("hydrate mode - hydrating");
leptos::mount_to_body(|cx| {
view! { cx, <App/> }
});
leptos::mount_to_body(|cx| {
view! { cx, <App/> }
});
}
}
else if #[cfg(feature = "csr")] {
@@ -35,5 +35,5 @@ cfg_if! {
view! { cx, <App /> }
});
}
}
}
}

10
examples/tailwind_csr_trunk/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

View File

@@ -0,0 +1,21 @@
[package]
name = "tailwind-csr-trunk"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { version = "0.2", features = [
"serde",
"csr",
] }
leptos_meta = { version = "0.2", features = ["csr"] }
leptos_router = { version = "0.2", features = ["csr"] }
log = "0.4"
gloo-net = { version = "0.2", features = ["http"] }
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2" }
console_log = { version = "0.2"}
console_error_panic_hook = { version = "0.1"}

View File

@@ -0,0 +1,21 @@
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

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

View File

@@ -0,0 +1,70 @@
# Leptos Starter Template
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [trunk](https://github.com/thedodd/trunk) tool.
Install Tailwind and build the CSS:
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
Install trunk to client side render this bundle.
`cargo install trunk`
Then the site can be served with `trunk serve --open`
The browser will automatically open [http://127.0.0.1:8080//](http://127.0.0.1:8080//)
You can begin editing your app at `src/app.rs`.
## Installing Tailwind
You can install Tailwind using `npm`:
```bash
npm install -D tailwindcss
```
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
## Setting up with VS Code and Additional Tools
If you're using VS Code, add the following to your `settings.json`
```json
"emmet.includeLanguages": {
"rust": "html",
"*.rs": "html"
},
"tailwindCSS.includeLanguages": {
"rust": "html",
"*.rs": "html"
},
"files.associations": {
"*.rs": "rust"
},
"editor.quickSuggestions": {
"other": "on",
"comments": "on",
"strings": true
},
"css.validate": false,
```
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
Install "VS Browser" extension, a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
## Notes about Tooling
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
## Attribution
This is based on the original Tailwind example (../examples/tailwind)

View File

@@ -0,0 +1,74 @@
{
"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

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

View File

@@ -0,0 +1,107 @@
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

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

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link data-trunk rel="rust" data-wasm-opt="z" />
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico" />
<link data-trunk rel="css" href="/style/output.css" />
<title>Leptos • Counter with Tailwind</title>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,43 @@
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! {
cx,
<Stylesheet id="leptos" href="/pkg/tailwind.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Router>
<Routes>
<Route path="" view= move |cx| view! { cx, <Home/> }/>
</Routes>
</Router>
}
}
#[component]
fn Home(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<div class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
<button
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
on:click=move |_| set_count.update(|count| *count += 1)
>
"Something's here | "
{move || if count() == 0 {
"Click me!".to_string()
} else {
count().to_string()
}}
" | Some more text"
</button>
</div>
}
}

View File

@@ -0,0 +1,15 @@
mod app;
use app::*;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("csr mode - mounting to body");
mount_to_body(|cx| {
view! { cx, <App /> }
});
}

View File

@@ -0,0 +1,583 @@
/*
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-0 {
margin-top: 0px;
margin-bottom: 0px;
}
.max-w-3xl {
max-width: 48rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-amber-600 {
--tw-bg-opacity: 1;
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
}
.p-6 {
padding: 1.5rem;
}
.px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:bg-sky-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
}

View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs"],
},
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -4,6 +4,15 @@ args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
clear = true
dependencies = ["check-debug", "check-release"]
[tasks.check-debug]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-release]
command = "cargo"
args = ["+nightly", "check-all-features", "--release"]
install_crate = "cargo-all-features"

View File

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

View File

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

View File

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

View File

@@ -150,12 +150,15 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
#[allow(deprecated)]
pub use leptos_dom::{
self, create_node_ref, debug_warn, document, error, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_idle_callback, set_interval,
set_timeout, window_event_listener,
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,

View File

@@ -116,9 +116,12 @@ where
let suspense_context = use_context::<SuspenseContext>(cx)
.expect("there to be a SuspenseContext");
if is_first_run(&first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only();
if cfg!(feature = "hydrate") || !first_run.get() {
*prev_children.borrow_mut() = Some(frag.nodes.clone());
}
if is_first_run(&first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only()
|| cfg!(feature = "csr");
if !has_local_only || child_runs.get() > 0 {
first_run.set(false);
}
@@ -138,17 +141,21 @@ fn is_first_run(
first_run: &Rc<Cell<bool>>,
suspense_context: &SuspenseContext,
) -> bool {
match (
first_run.get(),
cfg!(feature = "hydrate"),
suspense_context.has_local_only(),
) {
(false, _, _) => false,
// is in hydrate mode, and has non-local resources (so, has streamed)
(_, false, false) => false,
// is in hydrate mode, but with only local resources (so, has not streamed)
(_, false, true) => true,
// either SSR or client mode: it's the first run
(_, true, _) => true,
if cfg!(feature = "csr") {
false
} else {
match (
first_run.get(),
cfg!(feature = "hydrate"),
suspense_context.has_local_only(),
) {
(false, _, _) => false,
// SSR and has non-local resources (so, has streamed)
(_, false, false) => false,
// SSR but with only local resources (so, has not streamed)
(_, false, true) => true,
// hydrate: it's the first run
(_, true, _) => true,
}
}
}

View File

@@ -207,7 +207,10 @@ where
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
&& child
.get_closing_node()
.next_non_view_marker_sibling()
.as_ref()
!= Some(&closing);
// If the previous child was a text node, we would like to
@@ -241,7 +244,7 @@ where
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_sibling()
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
@@ -300,7 +303,7 @@ where
&& new_child.get_text().is_some()
{
let t = closing
.previous_sibling()
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>();
@@ -364,3 +367,49 @@ where
View::CoreComponent(crate::CoreComponent::DynChild(component))
}
}
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use web_sys::Node;
trait NonViewMarkerSibling {
fn next_non_view_marker_sibling(&self) -> Option<Node>;
fn previous_non_view_marker_sibling(&self) -> Option<Node>;
}
impl NonViewMarkerSibling for web_sys::Node {
fn next_non_view_marker_sibling(&self) -> Option<Node> {
cfg_if! {
if #[cfg(debug_assertions)] {
self.next_sibling().and_then(|node| {
if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") {
node.next_sibling()
} else {
Some(node)
}
})
} else {
self.next_sibling()
}
}
}
fn previous_non_view_marker_sibling(&self) -> Option<Node> {
cfg_if! {
if #[cfg(debug_assertions)] {
self.previous_sibling().and_then(|node| {
if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") {
node.previous_sibling()
} else {
Some(node)
}
})
} else {
self.previous_sibling()
}
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
//! A variety of DOM utility functions.
use crate::{is_server, window};
use leptos_reactive::{on_cleanup, Scope};
use std::time::Duration;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
@@ -80,10 +81,33 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
.checked()
}
/// Runs the given function between the next repaint
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
/// Handle that is generated by [request_animation_frame_with_handle] and can
/// be used to cancel the animation frame request.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnimationFrameRequestHandle(i32);
impl AnimationFrameRequestHandle {
/// Cancels the animation frame request to which this refers.
/// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame)
pub fn cancel(&self) {
_ = window().cancel_animation_frame(self.0);
}
}
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
_ = request_animation_frame_with_handle(cb);
}
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
/// returning a cancelable handle.
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_animation_frame_with_handle(
cb: impl FnOnce() + 'static,
) -> Result<AnimationFrameRequestHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -95,13 +119,38 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
}
let cb = Closure::once_into_js(cb);
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
window()
.request_animation_frame(cb.as_ref().unchecked_ref())
.map(AnimationFrameRequestHandle)
}
/// Queues the given function during an idle period
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
/// Handle that is generated by [request_idle_callback_with_handle] and can be
/// used to cancel the idle callback.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IdleCallbackHandle(u32);
impl IdleCallbackHandle {
/// Cancels the idle callback to which this refers.
/// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelIdleCallback)
pub fn cancel(&self) {
window().cancel_idle_callback(self.0);
}
}
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
_ = request_idle_callback_with_handle(cb);
}
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
/// returning a cancelable handle.
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_idle_callback_with_handle(
cb: impl Fn() + 'static,
) -> Result<IdleCallbackHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -113,7 +162,21 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
window()
.request_idle_callback(cb.as_ref().unchecked_ref())
.map(IdleCallbackHandle)
}
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TimeoutHandle(i32);
impl TimeoutHandle {
/// Cancels the timeout to which this refers.
/// See [`clearTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout)
pub fn clear(&self) {
window().clear_timeout_with_handle(self.0);
}
}
/// Executes the given function after the given duration of time has passed.
@@ -123,6 +186,19 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
_ = set_timeout_with_handle(cb, duration);
}
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout_with_handle(
cb: impl FnOnce() + 'static,
duration: Duration,
) -> Result<TimeoutHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -134,10 +210,83 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
}
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
);
window()
.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)
.map(TimeoutHandle)
}
/// "Debounce" a callback function. This will cause it to wait for a period of `delay`
/// after it is called. If it is called again during that period, it will wait
/// `delay` before running, and so on. This can be used, for example, to wrap event
/// listeners to prevent them from firing constantly as you type.
///
/// ```
/// use leptos::{leptos_dom::helpers::debounce, *};
///
/// #[component]
/// fn DebouncedButton(cx: Scope) -> impl IntoView {
/// let delay = std::time::Duration::from_millis(250);
/// let on_click = debounce(cx, delay, move |_| {
/// log!("...so many clicks!");
/// });
///
/// view! { cx,
/// <button on:click=on_click>"Click me"</button>
/// }
/// }
/// ```
pub fn debounce<T: 'static>(
cx: Scope,
delay: Duration,
mut cb: impl FnMut(T) + 'static,
) -> impl FnMut(T) {
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |value| {
let _guard = span.enter();
cb(value);
};
}
}
let cb = Rc::new(RefCell::new(cb));
let timer = Rc::new(Cell::new(None::<TimeoutHandle>));
on_cleanup(cx, {
let timer = Rc::clone(&timer);
move || {
if let Some(timer) = timer.take() {
timer.clear();
}
}
});
move |arg| {
if let Some(timer) = timer.take() {
timer.clear();
}
let handle = set_timeout_with_handle(
{
let cb = Rc::clone(&cb);
move || {
cb.borrow_mut()(arg);
}
},
delay,
);
if let Ok(handle) = handle {
timer.set(Some(handle));
}
}
}
/// Handle that is generated by [set_interval] and can be used to clear the interval.
@@ -152,12 +301,16 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[deprecated = "use set_interval_with_handle() instead. In the future, \
set_interval() will no longer return a handle, for consistency \
with other timer helper functions."]
pub fn set_interval(
cb: impl Fn() + 'static,
duration: Duration,
@@ -181,6 +334,36 @@ pub fn set_interval(
Ok(IntervalHandle(handle))
}
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_interval_with_handle(
cb: impl Fn() + 'static,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
let handle = window()
.set_interval_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)?;
Ok(IntervalHandle(handle))
}
/// Adds an event listener to the `Window`.
#[cfg_attr(
debug_assertions,

View File

@@ -297,16 +297,40 @@ cfg_if! {
pub struct HtmlElement<El: ElementDescriptor> {
pub(crate) cx: Scope,
pub(crate) element: El,
#[educe(Debug(ignore))]
pub(crate) attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
#[educe(Debug(ignore))]
#[allow(clippy::type_complexity)]
pub(crate) children: SmallVec<[View; 4]>,
#[educe(Debug(ignore))]
pub(crate) prerendered: Option<Cow<'static, str>>,
pub(crate) children: ElementChildren,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>
}
#[derive(Clone, educe::Educe, PartialEq, Eq)]
#[educe(Default)]
pub(crate) enum ElementChildren {
#[educe(Default)]
Empty,
Children(Vec<View>),
InnerHtml(Cow<'static, str>),
Chunks(Vec<StringOrView>)
}
#[doc(hidden)]
#[derive(Clone)]
pub enum StringOrView {
String(Cow<'static, str>),
View(std::rc::Rc<dyn Fn() -> View>)
}
impl PartialEq for StringOrView {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(StringOrView::String(a), StringOrView::String(b)) => a == b,
_ => false
}
}
}
impl Eq for StringOrView {}
}
}
@@ -341,9 +365,8 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
Self {
cx,
attrs: smallvec![],
children: smallvec![],
children: Default::default(),
element,
prerendered: None,
#[cfg(debug_assertions)]
view_marker: None
}
@@ -353,6 +376,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
#[deprecated = "Use HtmlElement::from_chunks() instead."]
pub fn from_html(
cx: Scope,
element: El,
@@ -361,9 +385,27 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
Self {
cx,
attrs: smallvec![],
children: smallvec![],
children: ElementChildren::Chunks(vec![StringOrView::String(
html.into(),
)]),
element,
#[cfg(debug_assertions)]
view_marker: None,
}
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn from_chunks(
cx: Scope,
element: El,
chunks: impl IntoIterator<Item = StringOrView>,
) -> Self {
Self {
cx,
attrs: smallvec![],
children: ElementChildren::Chunks(chunks.into_iter().collect()),
element,
prerendered: Some(html.into()),
#[cfg(debug_assertions)]
view_marker: None,
}
@@ -407,7 +449,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
attrs,
children,
element,
prerendered,
#[cfg(debug_assertions)]
view_marker
} = self;
@@ -416,7 +457,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
cx,
attrs,
children,
prerendered,
element: AnyElement {
name: element.name(),
is_void: element.is_void(),
@@ -736,8 +776,23 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let mut this = self;
let children = &mut this.children;
this.children.push(child);
match children {
ElementChildren::Empty => {
*children = ElementChildren::Children(vec![child]);
}
ElementChildren::Children(ref mut children) => {
children.push(child);
}
_ => {
crate::debug_warn!(
"Dont call .child() on an HtmlElement if youve \
already called .inner_html() or \
HtmlElement::from_chunks()."
);
}
}
this
}
@@ -764,16 +819,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
{
let mut this = self;
let child = HtmlElement::from_html(
this.cx,
Custom {
name: "inner-html".into(),
id: Default::default(),
},
html,
);
this.children = smallvec![child.into_view(this.cx)];
this.children = ElementChildren::InnerHtml(html);
this
}
@@ -793,7 +839,6 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
element,
mut attrs,
children,
prerendered,
#[cfg(debug_assertions)]
view_marker,
..
@@ -811,8 +856,7 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
}
element.attrs = attrs;
element.children.extend(children);
element.prerendered = prerendered;
element.children = children;
#[cfg(debug_assertions)]
{

View File

@@ -33,6 +33,10 @@ pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey};
use leptos_reactive::Scope;
#[cfg(feature = "stable")]
use leptos_reactive::{
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
pub use logging::*;
pub use macro_helpers::*;
pub use node_ref::*;
@@ -138,6 +142,72 @@ where
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for ReadSignal<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "ReadSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for RwSignal<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "RwSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for Memo<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "Memo<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for Signal<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "Signal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
#[cfg(feature = "stable")]
impl<T> IntoView for MaybeSignal<T>
where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "MaybeSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
DynChild::new(move || self.get()).into_view(cx)
}
}
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
/// HTML element.
@@ -161,14 +231,15 @@ cfg_if! {
}
}
} else {
use crate::html::ElementChildren;
/// HTML element.
#[derive(Clone, PartialEq, Eq)]
pub struct Element {
name: Cow<'static, str>,
is_void: bool,
attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
children: Vec<View>,
prerendered: Option<Cow<'static, str>>,
children: ElementChildren,
id: HydrationKey,
#[cfg(debug_assertions)]
/// Optional marker for the view macro source, in debug mode.
@@ -189,8 +260,10 @@ cfg_if! {
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
for child in &self.children {
writeln!(pad_adapter, "{child:#?}")?;
if let ElementChildren::Children(children) = &self.children {
for child in children {
writeln!(pad_adapter, "{child:#?}")?;
}
}
write!(f, "</{}>", self.name)
@@ -239,7 +312,6 @@ impl Element {
attrs,
children,
id,
prerendered,
#[cfg(debug_assertions)]
view_marker,
} = self;
@@ -250,8 +322,7 @@ impl Element {
cx,
element,
attrs,
children: children.into_iter().collect(),
prerendered,
children,
#[cfg(debug_assertions)]
view_marker,
}
@@ -286,7 +357,6 @@ impl Element {
attrs: Default::default(),
children: Default::default(),
id: el.hydration_id().clone(),
prerendered: None,
#[cfg(debug_assertions)]
view_marker: None
}

View File

@@ -2,7 +2,10 @@
//! Server-side HTML rendering utilities.
use crate::{CoreComponent, HydrationCtx, IntoView, View};
use crate::{
html::{ElementChildren, StringOrView},
CoreComponent, HydrationCtx, IntoView, View,
};
use cfg_if::cfg_if;
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
use itertools::Itertools;
@@ -385,8 +388,19 @@ impl View {
}
}
View::Element(el) => {
let el_html = if let Some(prerendered) = el.prerendered {
prerendered
let el_html = if let ElementChildren::Chunks(chunks) =
el.children
{
chunks
.into_iter()
.map(|chunk| match chunk {
StringOrView::String(string) => string,
StringOrView::View(view) => {
view().render_to_string_helper()
}
})
.join("")
.into()
} else {
let tag_name = el.name;
@@ -421,11 +435,17 @@ impl View {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
.into()
} else {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string_helper())
.join("");
let children = match el.children {
ElementChildren::Empty => "".into(),
ElementChildren::Children(c) => c
.into_iter()
.map(View::render_to_string_helper)
.join("")
.into(),
ElementChildren::InnerHtml(h) => h,
// already handled this case above
ElementChildren::Chunks(_) => unreachable!(),
};
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
.into()

View File

@@ -2,10 +2,14 @@
//! Server-side HTML rendering utilities for in-order streaming and async rendering.
use crate::{ssr::render_serializers, CoreComponent, HydrationCtx, View};
use crate::{
html::{ElementChildren, StringOrView},
ssr::render_serializers,
CoreComponent, HydrationCtx, View,
};
use async_recursion::async_recursion;
use cfg_if::cfg_if;
use futures::{channel::mpsc::Sender, Stream, StreamExt};
use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt};
use itertools::Itertools;
use leptos_reactive::{
create_runtime, run_scope_undisposed, suspense::StreamChunk, RuntimeId,
@@ -93,7 +97,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
)
});
let (tx, rx) = futures::channel::mpsc::channel(1);
let (tx, rx) = futures::channel::mpsc::unbounded();
leptos_reactive::spawn_local(async move {
handle_chunks(tx, chunks).await;
});
@@ -122,14 +126,15 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
}
#[async_recursion(?Send)]
async fn handle_chunks(mut tx: Sender<String>, chunks: Vec<StreamChunk>) {
async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
let mut buffer = String::new();
for chunk in chunks {
match chunk {
StreamChunk::Sync(sync) => buffer.push_str(&sync),
StreamChunk::Async(suspended) => {
// add static HTML before the Suspense and stream it down
_ = tx.try_send(std::mem::take(&mut buffer));
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send async HTML chunk");
// send the inner stream
let suspended = suspended.await;
@@ -138,7 +143,8 @@ async fn handle_chunks(mut tx: Sender<String>, chunks: Vec<StreamChunk>) {
}
}
// send final sync chunk
_ = tx.try_send(std::mem::take(&mut buffer));
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send final HTML chunk");
}
impl View {
@@ -186,8 +192,17 @@ impl View {
format!("<!--leptos-view|{id}|open-->").into(),
));
}
if let Some(prerendered) = el.prerendered {
chunks.push(StreamChunk::Sync(prerendered))
if let ElementChildren::Chunks(el_chunks) = el.children {
for chunk in el_chunks {
match chunk {
StringOrView::String(string) => {
chunks.push(StreamChunk::Sync(string))
}
StringOrView::View(view) => {
view().into_stream_chunks_helper(cx, chunks);
}
}
}
} else {
let tag_name = el.name;
@@ -231,8 +246,19 @@ impl View {
chunks.push(StreamChunk::Sync(
format!("<{tag_name}{attrs}>").into(),
));
for child in el.children {
child.into_stream_chunks_helper(cx, chunks);
match el.children {
ElementChildren::Empty => {}
ElementChildren::Children(children) => {
for child in children {
child.into_stream_chunks_helper(cx, chunks);
}
}
ElementChildren::InnerHtml(inner_html) => {
chunks.push(StreamChunk::Sync(inner_html));
}
// handled above
ElementChildren::Chunks(_) => unreachable!(),
}
chunks.push(StreamChunk::Sync(

View File

@@ -2,10 +2,6 @@ use crate::node::{LAttributeValue, LNode};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
// TODO: insertion and removal code are still somewhat broken
// namely, it will tend to remove and move or mutate nodes,
// which causes a bit of a problem for DynChild etc.
#[derive(Debug, Default)]
struct OldChildren(IndexMap<LNode, Vec<usize>>);
@@ -58,7 +54,7 @@ impl LNode {
.collect(),
},
LNode::Text(_)
| LNode::Component(_, _)
| LNode::Component { .. }
| LNode::DynChild(_) => ReplacementNode::Html(self.to_html()),
},
}
@@ -80,10 +76,19 @@ impl LNode {
child.add_old_children(new_path, positions);
}
}
// only need to insert dynamic content, as these might change
LNode::Component(_, _) | LNode::DynChild(_) => {
// need to insert dynamic content and children, as these might change
LNode::DynChild(_) => {
positions.0.insert(self.clone(), path);
}
LNode::Component { children, .. } => {
positions.0.insert(self.clone(), path.clone());
for (idx, child) in children.iter().enumerate() {
let mut new_path = path.clone();
new_path.push(idx);
child.add_old_children(new_path, positions);
}
}
// can just create text nodes, whatever
LNode::Text(_) => {}
}
@@ -148,6 +153,28 @@ impl LNode {
.collect()
}
// components + dynamic context: no patches
(
LNode::Component {
name: old_name,
children: old_children,
..
},
LNode::Component {
name: new_name,
children: new_children,
..
},
) if old_name == new_name => {
let mut path = path.to_vec();
path.push(0);
path.push(0);
LNode::diff_children(
&path,
old_children,
new_children,
orig_children,
)
}
_ => vec![],
}
}

View File

@@ -20,7 +20,11 @@ pub enum LNode {
},
// don't need anything; skipped during patching because it should
// contain its own view macros
Component(String, Vec<(String, String)>),
Component {
name: String,
props: Vec<(String, String)>,
children: Vec<LNode>,
},
DynChild(String),
}
@@ -71,9 +75,14 @@ impl LNode {
}
Node::Element(el) => {
if is_component_node(&el) {
views.push(LNode::Component(
el.name.to_string(),
el.attributes
let mut children = Vec::new();
for child in el.children {
LNode::parse_node(child, &mut children)?;
}
views.push(LNode::Component {
name: el.name.to_string(),
props: el
.attributes
.into_iter()
.filter_map(|attr| match attr {
Node::Attribute(attr) => Some((
@@ -83,7 +92,8 @@ impl LNode {
_ => None,
})
.collect(),
));
children,
});
} else {
let name = el.name.to_string();
let mut attrs = Vec::new();
@@ -125,7 +135,7 @@ impl LNode {
match self {
LNode::Fragment(frag) => frag.iter().map(LNode::to_html).collect(),
LNode::Text(text) => text.to_owned(),
LNode::Component(name, _) => format!(
LNode::Component { name, .. } => format!(
"<!--<{name}>--><pre>&lt;{name}/&gt; will load once Rust code \
has been compiled.</pre><!--</{name}>-->"
),

View File

@@ -2,7 +2,8 @@ console.log("[HOT RELOADING] Connected to server.");
function patch(json) {
try {
const views = JSON.parse(json);
for (const [id, patches] of views) {
for (const [id, patches] of views) {
console.log("[HOT RELOAD]", patches);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
open = `leptos-view|${id}|open`,
close = `leptos-view|${id}|close`;
@@ -250,7 +251,33 @@ function patch(json) {
node: walker.currentNode
});
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
if (walker.currentNode.textContent.trim().endsWith("-children|open")) {
const startingName = walker.currentNode.textContent.trim();
const componentName = startingName.replace("-children|open").replace("leptos-view|");
const endingName = `leptos-view|${componentName}-children|close`;
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == endingName) {
depth--;
} else if (walker.currentNode.textContent.trim() == startingName) {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "fragment",
start: start.nextSibling,
end: end.previousSibling,
children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling)
});
}
} else if (walker.currentNode.textContent.trim() == "<() />") {
actualChildren.push({
type: "unit",
@@ -326,7 +353,7 @@ function patch(json) {
return actualChildren;
}
function childAtPath(element, path) {
function childAtPath(element, path) {
if (path.length == 0) {
return element;
} else if (element.children) {

View File

@@ -130,6 +130,7 @@ impl ToTokens for Model {
let mut body = body.to_owned();
body.sig.ident = format_ident!("__{}", body.sig.ident);
#[allow(clippy::redundant_clone)] // false positive
let body_name = body.sig.ident.clone();
let (_, generics, where_clause) = body.sig.generics.split_for_impl();
@@ -153,6 +154,7 @@ impl ToTokens for Model {
if cfg!(feature = "tracing") {
(
quote! {
#[allow(clippy::let_with_type_underscore)]
#[cfg_attr(
debug_assertions,
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)

View File

@@ -267,32 +267,55 @@ fn root_element_to_tokens_ssr(
if is_component_node(node) {
component_to_tokens(cx, node, global_class)
} else {
let mut template = String::new();
let mut holes = Vec::<TokenStream>::new();
let mut exprs_for_compiler = Vec::<TokenStream>::new();
let mut template = String::new();
let mut holes = Vec::new();
let mut chunks = Vec::new();
element_to_tokens_ssr(
cx,
node,
&mut template,
&mut holes,
&mut chunks,
&mut exprs_for_compiler,
true,
global_class,
);
let template = if holes.is_empty() {
quote! {
#template
// push final chunk
if !template.is_empty() {
chunks.push(SsrElementChunks::String { template, holes })
}
let chunks = chunks.into_iter().map(|chunk| match chunk {
SsrElementChunks::String { template, holes } => {
if holes.is_empty() {
quote! {
leptos::leptos_dom::html::StringOrView::String(#template.into())
}
} else {
quote! {
leptos::leptos_dom::html::StringOrView::String(
format!(
#template,
#(#holes),*
)
.into()
)
}
}
}
} else {
quote! {
format!(
#template,
#(#holes)*
)
}
};
SsrElementChunks::View(view) => {
quote! {
#[allow(unused_braces)]
{
let view = #view;
leptos::leptos_dom::html::StringOrView::View(std::rc::Rc::new(move || view.clone()))
}
}
},
});
let tag_name = node.name.to_string();
let is_custom_element = is_custom_element(&tag_name);
@@ -328,27 +351,42 @@ fn root_element_to_tokens_ssr(
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, #full_name, #template)#view_marker
::leptos::HtmlElement::from_chunks(#cx, #full_name, [#(#chunks),*])#view_marker
}
}
}
}
enum SsrElementChunks {
String {
template: String,
holes: Vec<TokenStream>,
},
View(TokenStream),
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens_ssr(
cx: &Ident,
node: &NodeElement,
template: &mut String,
holes: &mut Vec<TokenStream>,
chunks: &mut Vec<SsrElementChunks>,
exprs_for_compiler: &mut Vec<TokenStream>,
is_root: bool,
global_class: Option<&TokenTree>,
) {
if is_component_node(node) {
template.push_str("{}");
let component = component_to_tokens(cx, node, global_class);
holes.push(quote! {
{#component}.into_view(cx).render_to_string(cx),
})
if !template.is_empty() {
chunks.push(SsrElementChunks::String {
template: std::mem::take(template),
holes: std::mem::take(holes),
})
}
chunks.push(SsrElementChunks::View(quote! {
{#component}.into_view(#cx)
}));
} else {
let tag_name = node
.name
@@ -374,9 +412,9 @@ fn element_to_tokens_ssr(
// insert hydration ID
let hydration_id = if is_root {
quote! { leptos::leptos_dom::HydrationCtx::peek(), }
quote! { leptos::leptos_dom::HydrationCtx::peek() }
} else {
quote! { leptos::leptos_dom::HydrationCtx::id(), }
quote! { leptos::leptos_dom::HydrationCtx::id() }
};
match node
.attributes
@@ -404,20 +442,23 @@ fn element_to_tokens_ssr(
let value = inner_html.as_ref();
holes.push(quote! {
(#value).into_attribute(cx).as_nameless_value_string().unwrap_or_default(),
(#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default()
})
} else {
for child in &node.children {
match child {
Node::Element(child) => element_to_tokens_ssr(
cx,
child,
template,
holes,
exprs_for_compiler,
false,
global_class,
),
Node::Element(child) => {
element_to_tokens_ssr(
cx,
child,
template,
holes,
chunks,
exprs_for_compiler,
false,
global_class,
);
}
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&html_escape::encode_safe(
@@ -428,7 +469,7 @@ fn element_to_tokens_ssr(
let value = text.value.as_ref();
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx),
#value.into_view(#cx).render_to_string(#cx)
})
}
}
@@ -436,14 +477,23 @@ fn element_to_tokens_ssr(
if let Some(value) = value_to_string(&block.value) {
template.push_str(&value);
} else {
template.push_str("{}");
let value = block.value.as_ref();
holes.push(quote! {
#value.into_view(#cx).render_to_string(#cx),
})
if !template.is_empty() {
chunks.push(SsrElementChunks::String {
template: std::mem::take(template),
holes: std::mem::take(holes),
})
}
chunks.push(SsrElementChunks::View(quote! {
{#value}.into_view(#cx)
}));
}
}
Node::Fragment(_) => todo!(),
Node::Fragment(_) => abort!(
Span::call_site(),
"You can't nest a fragment inside an element."
),
_ => {}
}
}
@@ -498,7 +548,7 @@ fn attribute_to_tokens_ssr<'a>(
&{#value}.into_attribute(#cx)
.as_nameless_value_string()
.map(|a| format!("{}=\"{}\"", #name, leptos::leptos_dom::ssr::escape_attr(&a)))
.unwrap_or_default(),
.unwrap_or_default()
})
}
} else {
@@ -618,9 +668,9 @@ fn set_class_attribute_ssr(
template.push_str(" {}");
let value = value.as_ref();
holes.push(quote! {
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
.unwrap_or_default(),
.unwrap_or_default()
});
}
}
@@ -628,13 +678,13 @@ fn set_class_attribute_ssr(
for (_span, name, value) in &class_attrs {
template.push_str(" {}");
holes.push(quote! {
(cx, #value).into_class(#cx).as_value_string(#name),
(#cx, #value).into_class(#cx).as_value_string(#name)
});
}
if let Some(dyn_global_class) = dyn_global_class {
template.push_str(" {}");
holes.push(quote! { #dyn_global_class, });
holes.push(quote! { #dyn_global_class });
}
template.push('"');
@@ -1053,6 +1103,15 @@ pub(crate) fn component_to_tokens(
let children = if node.children.is_empty() {
quote! {}
} else {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let marker = format!("<{component_name}/>-children");
let view_marker = quote! { .with_view_marker(#marker) };
} else {
let view_marker = quote! {};
}
}
let children = fragment_to_tokens(
cx,
span,
@@ -1071,7 +1130,7 @@ pub(crate) fn component_to_tokens(
.children({
#(#clonables)*
Box::new(move |#cx| #children)
Box::new(move |#cx| #children #view_marker)
})
}
};

View File

@@ -20,7 +20,7 @@ fn Component(
#[test]
fn component() {
let cp = ComponentProps::builder().into("").strip_option(9).build();
assert_eq!(cp.optional, false);
assert!(!cp.optional);
assert_eq!(cp.optional_no_strip, None);
assert_eq!(cp.strip_option, Some(9));
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());

View File

@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
use crate::{
create_effect, node::NodeId, on_cleanup, with_runtime, AnyComputation,
RuntimeId, Scope, SignalGet, SignalGetUntracked, SignalStream, SignalWith,
SignalWithUntracked,
RuntimeId, Scope, SignalDispose, SignalGet, SignalGetUntracked,
SignalStream, SignalWith, SignalWithUntracked,
};
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
@@ -412,6 +412,12 @@ impl<T: Clone> SignalStream<T> for Memo<T> {
}
}
impl<T> SignalDispose for Memo<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl_get_fn_traits![Memo];
pub(crate) struct MemoState<T, F>

View File

@@ -62,6 +62,7 @@ pub(crate) struct Runtime {
RefCell<SecondaryMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
pub pending_effects: RefCell<Vec<NodeId>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
pub batching: Cell<bool>,
}
// This core Runtime impl block handles all the work of marking and updating
@@ -186,12 +187,12 @@ impl Runtime {
let current_observer = self.observer.get();
// mark self dirty
if let Some(mut current_node) = nodes.get_mut(node) {
if let Some(current_node) = nodes.get_mut(node) {
Runtime::mark(
node,
&mut current_node,
current_node,
ReactiveNodeState::Dirty,
&mut *pending_effects,
&mut pending_effects,
current_observer,
);
@@ -200,10 +201,10 @@ impl Runtime {
let mut descendants = Default::default();
Runtime::gather_descendants(&subscribers, node, &mut descendants);
for descendant in descendants {
if let Some(mut node) = nodes.get_mut(descendant) {
if let Some(node) = nodes.get_mut(descendant) {
Runtime::mark(
descendant,
&mut node,
node,
ReactiveNodeState::Check,
&mut pending_effects,
current_observer,
@@ -248,12 +249,22 @@ impl Runtime {
pub(crate) fn run_effects(runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
let effects = runtime.pending_effects.take();
for effect_id in effects {
runtime.update_if_necessary(effect_id);
}
runtime.run_your_effects();
});
}
pub(crate) fn run_your_effects(&self) {
let effects = self.pending_effects.take();
for effect_id in effects {
self.update_if_necessary(effect_id);
}
}
pub(crate) fn dispose_node(&self, node: NodeId) {
self.node_sources.borrow_mut().remove(node);
self.node_subscribers.borrow_mut().remove(node);
self.nodes.borrow_mut().remove(node);
}
}
impl Debug for Runtime {

View File

@@ -442,6 +442,25 @@ impl Scope {
.ok()
.flatten()
}
/// Batches any reactive updates, preventing effects from running until the whole
/// function has run. This allows you to prevent rerunning effects if multiple
/// signal updates might cause the same effect to run.
///
/// # Panics
/// Panics if the runtime this scope belongs to has already been disposed.
pub fn batch<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, move |runtime| {
runtime.batching.set(true);
let val = f();
runtime.batching.set(false);
runtime.run_your_effects();
val
})
.expect(
"tried to run a batched update in a runtime that has been disposed",
)
}
}
impl fmt::Debug for ScopeDisposer {

View File

@@ -274,6 +274,16 @@ pub trait SignalStream<T> {
fn to_stream(&self, cx: Scope) -> Pin<Box<dyn Stream<Item = T>>>;
}
/// This trait allows disposing a signal before its [Scope] has been disposed.
pub trait SignalDispose {
/// Disposes of the signal. This:
/// 1. Detaches the signal from the reactive graph, preventing it from triggering
/// further updates; and
/// 2. Drops the value contained in the signal.
#[track_caller]
fn dispose(self);
}
/// Creates a signal, the basic reactive primitive.
///
/// A signal is a piece of data that may change over time,
@@ -725,6 +735,12 @@ impl<T: Clone> SignalStream<T> for ReadSignal<T> {
}
}
impl<T> SignalDispose for ReadSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> ReadSignal<T>
where
T: 'static,
@@ -1025,6 +1041,12 @@ impl<T> SignalSet<T> for WriteSignal<T> {
}
}
impl<T> SignalDispose for WriteSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> Clone for WriteSignal<T> {
fn clone(&self) -> Self {
Self {
@@ -1598,6 +1620,12 @@ impl<T: Clone> SignalStream<T> for RwSignal<T> {
}
}
impl<T> SignalDispose for RwSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> RwSignal<T> {
/// Returns a read-only handle to the signal.
///
@@ -1884,7 +1912,7 @@ impl NodeId {
runtime.mark_dirty(*self);
// notify subscribers
if updated.is_some() {
if updated.is_some() && !runtime.batching.get() {
Runtime::run_effects(runtime_id);
};
updated

View File

@@ -1,7 +1,7 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_memo, create_runtime, create_scope,
create_signal,
create_isomorphic_effect, create_memo, create_runtime, create_rw_signal,
create_scope, create_signal, SignalSet,
};
#[cfg(not(feature = "stable"))]
@@ -91,3 +91,45 @@ fn untrack_mutes_effect() {
})
.dispose()
}
#[cfg(not(feature = "stable"))]
#[test]
fn batching_actually_batches() {
use std::{cell::Cell, rc::Rc};
create_scope(create_runtime(), |cx| {
let first_name = create_rw_signal(cx, "Greg".to_string());
let last_name = create_rw_signal(cx, "Johnston".to_string());
// simulate an arbitrary side effect
let count = Rc::new(Cell::new(0));
create_isomorphic_effect(cx, {
let count = count.clone();
move |_| {
_ = first_name();
_ = last_name();
count.set(count.get() + 1);
}
});
// runs once initially
assert_eq!(count.get(), 1);
// individual updates run effect once each
first_name.set("Alice".to_string());
assert_eq!(count.get(), 2);
last_name.set("Smith".to_string());
assert_eq!(count.get(), 3);
// batched effect only runs twice
cx.batch(move || {
first_name.set("Bob".to_string());
last_name.set("Williams".to_string());
});
assert_eq!(count.get(), 4);
})
.dispose()
}

View File

@@ -286,7 +286,7 @@ where
let pending = create_rw_signal(cx, false);
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(async move { fut.await }) as Pin<Box<dyn Future<Output = O>>>
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
Action(store_value(

View File

@@ -297,7 +297,7 @@ where
let submissions = create_rw_signal(cx, Vec::new());
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(async move { fut.await }) as Pin<Box<dyn Future<Output = O>>>
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
MultiAction(store_value(

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.2.3"
version = "0.2.4"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.2.3"
version = "0.2.4"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"