Compare commits

..

629 Commits

Author SHA1 Message Date
Greg Johnston
2cc6ccaacb feat: custom_view macro to allow implementing view types on arbitrary data 2024-08-04 21:05:04 -04:00
Greg Johnston
1f4c410f78 Merge pull request #2607 from leptos-rs/leptos_0.7
(draft) Leptos 0.7
2024-08-04 11:45:22 -04:00
Corvus
3dbedfc871 feat: reintroduce custom events (#2762) 2024-08-04 08:21:10 -04:00
luoxiaozero
026152d20e fix: correctly rebuild reactive attributes to avoid stale signals (#2763) 2024-08-04 08:16:02 -04:00
luoxiaozero
c70009243a fix: allow svg elements to use inner_html (#2764) 2024-08-04 08:14:22 -04:00
Greg Johnston
dcdad73476 chore: fmt 2024-08-03 21:55:07 -04:00
Corvus
42a8e49e1a feat: provide deprecated watch function (#2761) 2024-08-03 21:24:46 -04:00
Greg Johnston
9ba894764e chore: fmt 2024-08-03 11:52:21 -04:00
Greg Johnston
1d97494e19 fix: update syntax now that dispatch returns something 2024-08-03 11:52:09 -04:00
Greg Johnston
982bfd8011 feat: allow aborting dispatched actions (closes #2746) 2024-08-03 10:59:54 -04:00
Greg Johnston
a0ad927097 fix: generated tests should not use old APIs 2024-08-03 10:48:11 -04:00
Greg Johnston
4845459511 chore: update tests 2024-08-03 10:11:29 -04:00
Greg Johnston
54e4205541 chore: update tests 2024-08-02 11:16:35 -04:00
Greg Johnston
25f0186098 chore: update tests 2024-08-02 10:29:48 -04:00
Greg Johnston
00fb8f29d3 fix: do not pass SendWrappers through Suspense (closes #2756) 2024-08-02 10:27:35 -04:00
Greg Johnston
82ea4eb7ce chore: fix ssr tests 2024-08-02 09:40:18 -04:00
Greg Johnston
34382c0c23 chore: fix ssr tests 2024-08-02 09:24:51 -04:00
Greg Johnston
605e4b1eec chore: update examples and doctests 2024-08-02 09:07:23 -04:00
Greg Johnston
71ca02a432 chore: clippy 2024-08-01 22:25:16 -04:00
Greg Johnston
ac751dd0d3 chore(ci): update nightly 2024-08-01 21:28:19 -04:00
Greg Johnston
114df8797c chore: clippy 2024-08-01 21:25:35 -04:00
Greg Johnston
35b457aa82 chore: update examples and doctests 2024-08-01 20:59:39 -04:00
Greg Johnston
c43b71d3fa chore: update examples and doctests 2024-08-01 20:45:40 -04:00
Greg Johnston
5ad17d6b6c fix: correct thread-local behavior for Effects (closes #2754) 2024-08-01 19:43:10 -04:00
Greg Johnston
805be42f7a chore: fix doctest 2024-08-01 19:43:10 -04:00
Greg Johnston
bb63a1521e chore: update tests and doctests 2024-08-01 19:43:10 -04:00
Greg Johnston
05cd1bc6f0 chore: proper feature gating for imports 2024-08-01 19:43:10 -04:00
Greg Johnston
84ac64284c chore: update tests and doctests 2024-08-01 19:43:10 -04:00
Greg Johnston
8beb988ecc chore: update tests and doctests 2024-08-01 19:43:10 -04:00
Greg Johnston
05999069b7 feat: add create_slice functions 2024-08-01 19:43:10 -04:00
Greg Johnston
dbb4c79d9c chore: remove unused features 2024-08-01 19:43:10 -04:00
Greg Johnston
d1d6126259 chore: add makefile for CI 2024-08-01 19:43:10 -04:00
Greg Johnston
0e979a0767 chore: remove dead code from leptos_dom (now found in tachys) 2024-08-01 19:43:10 -04:00
Greg Johnston
3a4ad07a91 chore: remove unused dependencies 2024-08-01 19:43:10 -04:00
Greg Johnston
f2f35cd785 chore: fix imports in tests 2024-08-01 19:43:10 -04:00
Greg Johnston
c953432659 chore: fmt 2024-08-01 19:43:10 -04:00
Greg Johnston
27f2e60d16 chore: clean up unused imports in tests 2024-08-01 19:43:10 -04:00
Greg Johnston
9330cf23b1 fix: set default storage type for SignalTypes 2024-08-01 19:43:10 -04:00
Greg Johnston
04f5207457 fix: be ready to complete navigation immediately 2024-08-01 19:43:09 -04:00
Greg Johnston
c88409a333 fix: update reactive views for new requirements 2024-08-01 19:43:09 -04:00
Greg Johnston
a2d3d9431c feat: support memos with !Send values 2024-08-01 19:43:09 -04:00
Greg Johnston
213365e4e9 chore: clippy 2024-08-01 19:43:09 -04:00
Greg Johnston
ab3e94dafa chore: clippy 2024-08-01 19:43:09 -04:00
Greg Johnston
3e05b5bcb4 feat: support class = in view macro 2024-08-01 19:43:09 -04:00
Greg Johnston
632ce0f401 fix merge issues 2024-08-01 19:43:09 -04:00
Greg Johnston
93c893f4b3 fix: use correct Request type in Actix extractor 2024-08-01 19:43:09 -04:00
Greg Johnston
4fa72a94fb feat: allow reusing the same endpoint for server functions with different HTTP verbs in their input encodings 2024-08-01 19:43:09 -04:00
Greg Johnston
24775fb59b fix: properly handle islands used in the body of other islands (closes #2725) 2024-08-01 19:43:09 -04:00
Greg Johnston
6c749f5e24 fix: provide a hydration context during route generation 2024-08-01 19:43:09 -04:00
Marc-Stefan Cassola
c22f20ac28 feat: add Effect::watch (#2732) 2024-08-01 19:43:09 -04:00
Greg Johnston
11011c2bda fix: correct async resolution for islands (closes #2734) 2024-08-01 19:43:09 -04:00
Greg Johnston
a51faea9a9 fix: abort when leaving *this* owner, but don't force it if already complete 2024-08-01 19:43:09 -04:00
Greg Johnston
dcc43e574b fix: correctly handle Action abort if it has already completed (closes #2740) 2024-08-01 19:43:09 -04:00
Greg Johnston
6cc0604497 fix: no longer necessary to 'seal' ErrorBoundary (closes #2738) 2024-08-01 19:43:09 -04:00
Chris
8c375534bb fix: leptos_router_macro::path into public api (#2737) 2024-08-01 19:43:09 -04:00
Greg Johnston
f2d6375d93 fix: never resolve LocalResource synchronously (closes #2736) 2024-08-01 19:43:09 -04:00
Greg Johnston
5fb80aaa40 fix: cancel action when original owner, not current owner, is cleaned up (closes #2739) 2024-08-01 19:43:09 -04:00
Greg Johnston
29b0dca1d8 fix: allow Suspense to remount before rebuilding (closes #2721) 2024-08-01 19:43:09 -04:00
Greg Johnston
a6a65ba562 fix: release borrow on shared context when hydrating island (closes #2733) 2024-08-01 19:43:09 -04:00
Greg Johnston
260c624461 fix: serializing island props (closes #2730) 2024-08-01 19:43:09 -04:00
Greg Johnston
0f50aced26 fix: tracking/notifications for AsyncDerived if they start with initial values, and prevent over-notification (closes #2716) 2024-08-01 19:43:09 -04:00
Greg Johnston
1aa2752842 fix: if island is not entry point, update hydration cursor correctly to include island node (closes #2724) 2024-08-01 19:43:09 -04:00
Greg Johnston
b92bfa4ea7 docs: suppress warning about wrong client-side mode after initial call, for things like Portal (closes #2725) 2024-08-01 19:43:09 -04:00
Greg Johnston
30cf1167f2 fix: correct re-exports for islands with props (closes #2723) 2024-08-01 19:43:09 -04:00
Greg Johnston
1c05389707 fix: prevent LocalResource from spawning a local task on the server, when it should only run on the client (closes #2717) 2024-08-01 19:43:09 -04:00
Greg Johnston
1534dd5261 fix: CSR <ErrorBoundary/> 2024-08-01 19:43:09 -04:00
Greg Johnston
8b9685e01d fix: throw_error version 2024-08-01 19:43:09 -04:00
Greg Johnston
d9b590b8e0 fix: unique IDs and correct hydration for <ErrorBoundary/> (closes #2704) 2024-08-01 19:43:09 -04:00
Greg Johnston
25bfc27544 fix: set <Title/> text and formatter in context during client-side rendering (closes #2715) 2024-08-01 19:43:09 -04:00
Greg Johnston
89bbdc58af feat: patch reactive stores in place, only notifying on changed fields 2024-08-01 19:43:09 -04:00
Greg Johnston
4fb00e29d6 chore: remove unused imports 2024-08-01 19:43:09 -04:00
Greg Johnston
54401c6f69 beta version 2024-08-01 19:43:09 -04:00
Greg Johnston
6a705e2a21 chore: clippy 2024-08-01 19:43:09 -04:00
Greg Johnston
43421c56d5 example: use a local signal for the input 2024-08-01 19:43:09 -04:00
Greg Johnston
4c4d3dcfa3 feat: mark branches in AnyView 2024-08-01 19:43:09 -04:00
Greg Johnston
55053da00c chore: clippy 2024-08-01 19:43:09 -04:00
Greg Johnston
be83b5f27e chore: remove unused imports 2024-08-01 19:43:09 -04:00
Greg Johnston
a4ce79769a fix: improve type inference for the default threadsafe signal case 2024-08-01 19:43:09 -04:00
Greg Johnston
e3f64188c2 chore: clippy 2024-08-01 19:43:09 -04:00
Greg Johnston
f7abe727d9 fix: add missing imports 2024-08-01 19:43:09 -04:00
Greg Johnston
eb29d84169 docs: fix cfg for docsrs 2024-08-01 19:43:09 -04:00
Greg Johnston
200047a8bc chore: remove unnecessary default generics 2024-08-01 19:43:09 -04:00
Greg Johnston
634ac1c4a3 chore: clippy and fmt 2024-08-01 19:43:09 -04:00
Saber Haj Rabiee
efe832e39a fix: hackernews_js_fetch example for leptos_0.7 (#2678) 2024-08-01 19:43:09 -04:00
Greg Johnston
1f2b13a976 feat: allow !Send signals 2024-08-01 19:42:51 -04:00
Greg Johnston
d4ec5e187b fix: rename nightly feature for const generic &'static str (d0c11bf6e3\#diff-7b65e42e2b87910c94950caf7f0687fda2f9f98f311099404f5c4afb4a36e50c) 2024-08-01 19:42:51 -04:00
Greg Johnston
4fe7fe725f chore: remove pub field in Suspend so that Suspend::new() must be used 2024-08-01 19:42:51 -04:00
Greg Johnston
a1ca8549a1 chore: fmt 2024-08-01 19:42:51 -04:00
Greg Johnston
e7a8067f9b fix: only create Future once initially, and poll it twice, rather than creating it twice 2024-08-01 19:42:51 -04:00
Greg Johnston
6be090079f chore: fmt 2024-08-01 19:42:51 -04:00
Greg Johnston
8635887ca7 feat: optional branch-marking in HTML to support initial work on client-side islands routing 2024-08-01 19:42:51 -04:00
Corvus
e3482b433b feat: reintroduce queue_microtask (#2703) 2024-08-01 19:42:51 -04:00
Greg Johnston
e6c2f8c614 fix: allow one-element tuples for route matching 2024-08-01 19:42:51 -04:00
Greg Johnston
28fcfe4a46 example: use path! macro in router example 2024-08-01 19:42:51 -04:00
Greg Johnston
75336bc265 fix: add HTML global on___ attributes 2024-08-01 19:42:51 -04:00
boyswan
f4f129caaf feat: add path! macro in router to parse string paths into tuples (#2694) 2024-08-01 19:42:51 -04:00
mahmoud-eltahawy
873aec5787 feat: allow using enums for StaticSegment by implementing AsPath (#2685) 2024-08-01 19:42:51 -04:00
Greg Johnston
a2385e4c42 fix: set None observer properly in ScopedFuture 2024-08-01 19:42:51 -04:00
Greg Johnston
d24f97b59f fix: remove unnecessary untrack in Show 2024-08-01 19:42:51 -04:00
Greg Johnston
51f368c5c5 fix: Suspend::new() in router 2024-08-01 19:42:51 -04:00
Greg Johnston
4107203da2 examples: update to Suspend::new() 2024-08-01 19:42:51 -04:00
Greg Johnston
efb699a319 docs: improved warning location 2024-08-01 19:42:51 -04:00
Greg Johnston
93e6456e19 fix: require Suspend::new() to ensure the Future is properly scoped at creation time, not at render time 2024-08-01 19:42:51 -04:00
Greg Johnston
b24ae7a5e3 fix: explicitly untrack the children of <Show/> 2024-08-01 19:42:51 -04:00
Greg Johnston
bf8d2e079c fix: custom elements should support any attribute names 2024-08-01 19:42:50 -04:00
Greg Johnston
7752ab78e3 fix: custom elements SSR 2024-08-01 19:42:50 -04:00
Greg Johnston
64bc2580ff docs: add tachys docs 2024-08-01 19:42:50 -04:00
Greg Johnston
ddb596feb5 chore: start with a default sandbox, to avoid panics in tests 2024-08-01 19:42:50 -04:00
Greg Johnston
dac4589194 docs: finish reactive graph docs for 0.7 2024-08-01 19:42:50 -04:00
Greg Johnston
8f0a8e05b4 docs: porting docs from 0.6 to 0.7 2024-08-01 19:42:50 -04:00
Greg Johnston
05d01141c5 chore: remove unused AsyncState 2024-08-01 19:42:50 -04:00
Bruno De Simone
66d6038f2d add allow too_many_arguments (#2684) 2024-08-01 19:42:50 -04:00
Greg Johnston
3b09312e1a chore: clippy 2024-08-01 19:42:50 -04:00
Greg Johnston
62cb361031 chore: clippy 2024-08-01 19:42:50 -04:00
Greg Johnston
04c67cb8b6 chore: clear warnings 2024-08-01 19:42:50 -04:00
Greg Johnston
efd060c955 feat: Suspend on style: and class: 2024-08-01 19:42:50 -04:00
Greg Johnston
6290c42159 fix: proper building of paths for nested fields 2024-08-01 19:42:50 -04:00
Greg Johnston
0a89f151be feat: type-erased store Field structs 2024-08-01 19:42:50 -04:00
Greg Johnston
c72c2f4803 fix: allow creating resources inside Suspense 2024-08-01 19:42:50 -04:00
Greg Johnston
c771ab7e71 examples: revert changes to counter 2024-08-01 19:42:50 -04:00
Bruno De Simone
2c4f11b238 remove FromRef implementation from LeptosRoutes axum impl (#2670) 2024-08-01 19:42:50 -04:00
luoxiaozero
12a9e06c5e feat: additional ARIA attributes (#2677) 2024-08-01 19:42:50 -04:00
Greg Johnston
3515469835 feat: iteration over reactive store list 2024-08-01 19:42:50 -04:00
Greg Johnston
e5c159f7a5 feat: add arena-allocated/Copy Store 2024-08-01 19:42:50 -04:00
Greg Johnston
6590749956 docs: initial work on porting docs from 0.6 to 0.7 2024-08-01 19:42:50 -04:00
Greg Johnston
db8f5e4899 feat: initial work on reactive stores 2024-08-01 19:42:50 -04:00
Greg Johnston
6ca3639c3e fix: improved API for unsync actions that doesn't require SendWrapper on input 2024-08-01 19:42:50 -04:00
Greg Johnston
37db7b5d0a chore: leptosfmt 2024-08-01 19:42:50 -04:00
Greg Johnston
8ac1564b90 fix: properly handle errors in streaming body responses 2024-08-01 19:42:50 -04:00
Greg Johnston
e2721d53bd fix: invalid p/ul relationship causing hydration issue 2024-08-01 19:42:50 -04:00
Saber Haj Rabiee
e1f3be6416 chore: cargo fmt (#2672) 2024-08-01 19:42:50 -04:00
Greg Johnston
9536480739 feat: use codee for shared ser-de codexes with leptos-use (and more possibilities in future) 2024-08-01 19:42:50 -04:00
Greg Johnston
5d3a1752c4 chore: remove unused dependencies 2024-08-01 19:42:50 -04:00
Greg Johnston
4b539b524b fix: was disposing of Suspense Owner too early 2024-08-01 19:42:50 -04:00
Greg Johnston
67fe4cc540 fix: rebuilding NodeRef 2024-08-01 19:42:50 -04:00
Greg Johnston
fa731d5018 feat: top-level Suspend without Suspense 2024-08-01 19:42:50 -04:00
Greg Johnston
ccf6703274 chore: clippy and clean up unused functions 2024-08-01 19:42:50 -04:00
Greg Johnston
504c958001 docs: update syntax for Html/Body 2024-08-01 19:42:50 -04:00
Greg Johnston
f7b16b726b feat: correct HTML rendering for spread attributes on <Body/> and <Html/> 2024-08-01 19:42:50 -04:00
Greg Johnston
e9c7b50dfd feat: attributes on body and html 2024-08-01 19:42:50 -04:00
Greg Johnston
208ab97867 feat: move to a channel-based implementation for meta 2024-08-01 19:42:50 -04:00
Greg Johnston
4a0f173bb5 feat: support Suspend in attributes 2024-08-01 19:42:50 -04:00
Greg Johnston
0cf3113812 feat: local resources with .await 2024-08-01 19:42:50 -04:00
Greg Johnston
87f9fa23d5 chore: cfg warnings 2024-08-01 19:42:50 -04:00
Greg Johnston
746bf8e453 fix: MaybeProp None case 2024-08-01 19:42:50 -04:00
Greg Johnston
8b9bcffbb9 reexport SVG and MathML element types 2024-08-01 19:42:50 -04:00
Greg Johnston
dc80e387e3 router alpha 3 2024-08-01 19:42:50 -04:00
Greg Johnston
2006eca1a0 Form component in 0.7 2024-08-01 19:42:50 -04:00
Greg Johnston
1dae77d6b4 fix: don't break Routes SSR now that it uses Suspend 2024-08-01 19:42:50 -04:00
Alicia Garcia-Raboso
2f58191a56 Implement Default trait for signals with a parameter that also implements Default (#2662)
Co-authored-by: alicia.garcia.raboso <alicia.garcia.raboso@bbva.com>
2024-08-01 19:42:50 -04:00
Greg Johnston
a68653b385 feat: automatically replace Suspense blocks if they are still waiting to be flushed, without JS (replaces PartiallyBlocked) 2024-08-01 19:42:50 -04:00
Greg Johnston
d7ca969848 blocking resources 2024-08-01 19:42:50 -04:00
Greg Johnston
fd48a61eef macro alpha 2 2024-08-01 19:42:50 -04:00
Greg Johnston
52a3f84de5 router alpha 2 2024-08-01 19:42:50 -04:00
Luke Naylor
f8283f4674 Add missing 'form' attribute for <mo> (#2660)
This attribute seems to be missing in the attribute
table on Mozilla Docs, however does appear in the
compatibility table lower down.
This attribute is also frequently used by temml,
a common generator for mathml content.
2024-08-01 19:42:50 -04:00
Greg Johnston
989f2989fa allow Outlet to be called multiple times 2024-08-01 19:42:50 -04:00
Greg Johnston
33a3708f91 fix: prevent panicking if conditionally rendering Outlet 2024-08-01 19:42:50 -04:00
Greg Johnston
4eea1f046d remove log 2024-08-01 19:42:50 -04:00
Greg Johnston
8f46288973 fix: ensure correct ownership chain when passing views through Outlet 2024-08-01 19:42:50 -04:00
Greg Johnston
059c8abd2f chore: clippy 2024-08-01 19:42:50 -04:00
Greg Johnston
6885777c75 support MaybeSignal in view 2024-08-01 19:42:50 -04:00
Kajetan Welc
ddc7abf081 fix: impl Copy for Callback (#2658) 2024-08-01 19:42:50 -04:00
Greg Johnston
180511e9bb fix: update imports and methods 2024-08-01 19:42:50 -04:00
Greg Johnston
381ff8a7b0 fix: trait import 2024-08-01 19:42:50 -04:00
Greg Johnston
3ed1ad7b7f impl From<T> for (Arc)Signal<T> 2024-08-01 19:42:50 -04:00
Greg Johnston
2ccf5e99a9 Revert "lazy Future construction for AsyncDerived"
This reverts commit 9e84e1f57c.
2024-08-01 19:42:50 -04:00
Greg Johnston
055701ebf6 fix: <option> struct generation 2024-08-01 19:42:50 -04:00
Greg Johnston
88af893703 fix: <option> struct generation 2024-08-01 19:42:50 -04:00
Lucas Åström
ce4fe632a2 Destructuring let (0.7) (#2655)
* Use `let()` syntax for bindings

This lets users use destructuring when binding more complex values, and we also get better IDE support.

* Update rstml
2024-08-01 19:42:50 -04:00
Greg Johnston
c76208aad0 fix: nonexistent feature 2024-08-01 19:42:50 -04:00
Greg Johnston
514c51ca30 fix: rebuilding AnyView 2024-08-01 19:42:50 -04:00
Greg Johnston
7e3781b5dd support wasm-only 2024-08-01 19:42:50 -04:00
Greg Johnston
97dc3cc2e5 support wasm-only 2024-08-01 19:42:50 -04:00
Greg Johnston
61e51cbe1c support wasm-only 2024-08-01 19:42:50 -04:00
Greg Johnston
efa6d603f9 any_spawner tick version 2024-08-01 19:42:50 -04:00
Greg Johnston
da045f7358 un-break circular dependency 2024-08-01 19:42:50 -04:00
Greg Johnston
8502745036 chore: update test output 2024-08-01 19:42:50 -04:00
Greg Johnston
0a5e6fd85a chore: unused import 2024-08-01 19:42:50 -04:00
Greg Johnston
64fc6cd514 restore fallback for compressed version 2024-08-01 19:42:50 -04:00
Greg Johnston
a2d8fde8cf docs: working on memo docs 2024-08-01 19:42:50 -04:00
Greg Johnston
44eae4c2ed 0.7.0-alpha 2024-08-01 19:42:50 -04:00
Greg Johnston
38d51b01d7 feat: support reactive and asynchronous ProtectedRoute conditions 2024-08-01 19:42:49 -04:00
Greg Johnston
61876dff10 fix: correct For behavior when mounting with siblings, and when clearing 2024-08-01 19:42:49 -04:00
Ben Wishovich
c676cf921d Make get_configuration sync (#2647)
* Made get_configuraiton sync

* Update examples
2024-08-01 19:42:49 -04:00
Greg Johnston
fc59cdaf61 examples: update directives tests 2024-08-01 19:42:49 -04:00
Greg Johnston
081f4ec550 chore: cargo fmt 2024-08-01 19:42:49 -04:00
Greg Johnston
598c59b9c2 make RemoveEventHandler a concrete type 2024-08-01 19:42:49 -04:00
Greg Johnston
9de6c5bb4a feat: add ElementExt to give access to the same view APIs at runtime that we do at compile time 2024-08-01 19:42:49 -04:00
Greg Johnston
f65eaec9ba feat: add ElementExt to give access to the same view APIs at runtime that we do at compile time 2024-08-01 19:42:49 -04:00
Greg Johnston
95756aa2f7 chore: cargo fmt 2024-08-01 19:42:49 -04:00
Greg Johnston
fd121fd8c1 docs: warn on unused RenderEffect 2024-08-01 19:42:49 -04:00
Greg Johnston
c1877354f0 chore: missing Debug implementations 2024-08-01 19:42:49 -04:00
Greg Johnston
be92dc56aa chore: suppress unnecessary .into() warning 2024-08-01 19:42:49 -04:00
Greg Johnston
165a593b32 cargo fmt 2024-08-01 19:42:49 -04:00
Greg Johnston
18b33c7606 updated directives example 2024-08-01 19:42:49 -04:00
brofrain
d2ee093132 fix: update HtmlViewState & BodyViewState as well 2024-08-01 19:42:49 -04:00
Kajetan Welc
83e0438527 fix: do not accidentally mount things before meta tags in the <head> when updating the DOM v2 2024-08-01 19:42:49 -04:00
Greg Johnston
095dc78893 remove log 2024-08-01 19:42:49 -04:00
Greg Johnston
3ebea79e05 preliminary work on directives (not useful yet until we have an ElementExt that allows you to do things declaratively from an Element 2024-08-01 19:42:49 -04:00
Greg Johnston
fe7c7c3a99 omit () entirely if it is the only child of an HTML element 2024-08-01 19:42:49 -04:00
Greg Johnston
8b142c72f0 fix: don't override a text node's 'next sibling after text' position if it's in Either, now that they don't have separate marker nodes 2024-08-01 19:42:49 -04:00
Greg Johnston
70655b57b1 fix: do not accidentally mount things before meta tags in the <head> when updating the DOM 2024-08-01 19:42:49 -04:00
Greg Johnston
c6192badfb fix docs for hydrate_islands 2024-08-01 19:42:49 -04:00
Greg Johnston
5b7f5e3db3 hackernews islands example 2024-08-01 19:42:49 -04:00
Greg Johnston
ae14644806 update static file serving in Axum examples 2024-08-01 19:42:21 -04:00
Greg Johnston
7ca810d8bd fix islands ci setup 2024-08-01 19:42:21 -04:00
Greg Johnston
04e09d2005 fix: remove extra comment at end of Suspense now that Either no longer requires it 2024-08-01 19:42:21 -04:00
Greg Johnston
2916873985 feat: provide static file handling/fallback directly in integration 2024-08-01 19:42:21 -04:00
Greg Johnston
2a558aa3f0 islands example 2024-08-01 19:42:21 -04:00
Greg Johnston
36d16d9253 remove unused tests and dependencies 2024-08-01 19:42:21 -04:00
Greg Johnston
722fd0f6c2 fix: () in templates 2024-08-01 19:42:21 -04:00
Greg Johnston
a42e371e79 chore: clippy 2024-08-01 19:42:21 -04:00
Greg Johnston
11119144d2 fix js-framework-benchmark for stable 2024-08-01 19:42:21 -04:00
Greg Johnston
a0b158f016 update hackernews_axum to 0.7 2024-08-01 19:42:21 -04:00
Greg Johnston
8dc7338b85 fix ErrorBoundary starting in error state in CSR 2024-08-01 19:42:21 -04:00
Greg Johnston
737949cff6 fix example tests 2024-08-01 19:42:21 -04:00
Greg Johnston
d7e17a2ec9 remove unnecessary logs 2024-08-01 19:42:21 -04:00
Greg Johnston
7c5b7fcbb1 update islands example 2024-08-01 19:42:21 -04:00
Greg Johnston
1182aff410 chore: unused hooks 2024-08-01 19:42:21 -04:00
Greg Johnston
bdcd4cb1cc regression test for 7094dee150 2024-08-01 19:42:21 -04:00
Greg Johnston
c74a791d9f fix: signals mark subscribers dirty, but don't say they're always dirty if they haven't changed 2024-08-01 19:42:21 -04:00
Greg Johnston
772a837050 make Routes fallback run lazily 2024-08-01 19:42:21 -04:00
Greg Johnston
92552deb0d make ErrorBoundary fallback run lazily 2024-08-01 19:42:21 -04:00
Greg Johnston
417d345b83 examples: errors_axum 2024-08-01 19:42:21 -04:00
Greg Johnston
3fb2d49d89 chore(ci): fix examples 2024-08-01 19:42:21 -04:00
Greg Johnston
27feaf4309 enable reactive-graph hydration when hydration is enabled 2024-08-01 19:42:21 -04:00
Greg Johnston
35f489a52e allow conversion directly from Arc signal types to MaybeSignal 2024-08-01 19:42:21 -04:00
Greg Johnston
ba8bd2bc82 expose Owner::shared_context() 2024-08-01 19:42:21 -04:00
Greg Johnston
76506c03e1 0.7 Provider component 2024-08-01 19:42:21 -04:00
Greg Johnston
4323e30133 fix tests 2024-08-01 19:42:21 -04:00
Greg Johnston
81c0947ce5 fix reactive styles 2024-08-01 19:42:21 -04:00
Greg Johnston
309a3d504a fix: correctly rebuild reactive attributes to avoid stale signals 2024-08-01 19:42:21 -04:00
Greg Johnston
2a236e043a type-erase RenderEffeect functions for binary size improvements 2024-08-01 19:42:21 -04:00
luoxiaozero
63f8da2fb5 feat: Attr exposes PhantomData field (#2641) 2024-08-01 19:42:21 -04:00
Greg Johnston
c9e32b66bf chore(ci): remove warnings in tests 2024-08-01 19:42:21 -04:00
Greg Johnston
a32c71539d feat: 0.7 query signals 2024-08-01 19:42:21 -04:00
Greg Johnston
f7ee0c4764 chore(ci): add Makefiles for smaller packages 2024-08-01 19:42:21 -04:00
Greg Johnston
7034375cdd chore(ci): only run semver checks if not labeled 'breaking' 2024-08-01 19:42:21 -04:00
Greg Johnston
3c9c5aaf83 chore: clippy 2024-08-01 19:42:21 -04:00
Greg Johnston
ce832cef21 rename from new_serde to new 2024-08-01 19:42:21 -04:00
Greg Johnston
10230d6d65 remove most remaining marker/placeholder elements 2024-08-01 19:42:21 -04:00
Greg Johnston
e4d25608df add trait impls and encodings for SharedValue 2024-08-01 19:42:21 -04:00
Greg Johnston
bd1601e892 default to SerdeJson encoding for resources, use new_str() for Str encoding 2024-08-01 19:42:21 -04:00
Greg Johnston
602ac60a85 feat: synchronous serialized values with SharedValue 2024-08-01 19:42:21 -04:00
Greg Johnston
9e4c0b86f2 whenever we create a new root Owner, set it as the current owner, which will make it the default owner (e.g., during SSR) instead of None 2024-08-01 19:42:21 -04:00
Greg Johnston
4a80c8b65b fix: can't memoize JS properties, because they can be set between signal updates by user input 2024-08-01 19:42:21 -04:00
Greg Johnston
f191bb8324 fix: correctly escape style and class attributes 2024-08-01 19:42:21 -04:00
Greg Johnston
1ff1d48e6e chore: clippy 2024-08-01 19:42:21 -04:00
Greg Johnston
df6a4628c3 don't require spawn_local for actios 2024-08-01 19:42:21 -04:00
Greg Johnston
e28e5ceb1e catch resource reads inside Signal during Suspense 2024-08-01 19:42:21 -04:00
Greg Johnston
e69f62b939 fix CSS file names 2024-08-01 19:42:21 -04:00
Greg Johnston
2c48b07186 update todo app csr 2024-08-01 19:42:21 -04:00
Greg Johnston
0d867ba016 fix: correctly escape text nodes, except in script/style tags 2024-08-01 19:42:21 -04:00
Greg Johnston
3f83ad7dda chore: clean up examples for CI 2024-08-01 19:42:21 -04:00
Greg Johnston
50403846c9 fix: provide matched route via context when rebuilding (so <A> works) 2024-08-01 19:42:21 -04:00
Greg Johnston
4ead16e5d3 unused 2024-08-01 19:42:21 -04:00
Greg Johnston
32f77cc42b refactor insert_before_this to find parent lazily, and use it for rebuilding reactive components by replacing their whole contents 2024-08-01 19:42:21 -04:00
Greg Johnston
d8834a0423 make sure SendWrapper supports Futures 2024-08-01 19:42:21 -04:00
Greg Johnston
0d665c9c05 move several complex examples into projects 2024-08-01 19:42:21 -04:00
Greg Johnston
a03d74494d update js-framework-benchmark example 2024-08-01 19:42:20 -04:00
Greg Johnston
131c83e28e fix test text 2024-08-01 19:42:20 -04:00
Greg Johnston
6d93185478 fix: custom Stream implementation for streaming resource data that supports nested data/multiple polled values, rather than taking it all at once at the beginning 2024-08-01 19:42:20 -04:00
Greg Johnston
202abd1d35 suspense_tests: actually wait for other resource in nested case 2024-08-01 19:42:20 -04:00
Greg Johnston
a50c6b0140 can save a lookup here 2024-08-01 19:42:20 -04:00
Greg Johnston
705ea3a3bb remove unused workspace member 2024-08-01 19:42:20 -04:00
Greg Johnston
cb788758df update workflows 2024-08-01 19:42:20 -04:00
Greg Johnston
1afdc4fe1e remove unused leptos_reactive integration 2024-08-01 19:42:20 -04:00
Greg Johnston
3382047857 remove old router files 2024-08-01 19:42:20 -04:00
Greg Johnston
a29ffc8dcb fix Cargo.toml after merge 2024-08-01 19:42:20 -04:00
Greg Johnston
a18dd6dfd7 re-enable all routes 2024-08-01 19:42:20 -04:00
Greg Johnston
626bcdc9ae chore: clean up warnings and logging 2024-08-01 19:42:20 -04:00
Greg Johnston
d6dce76725 reverted Fn()/FnMut() change 2024-08-01 19:42:20 -04:00
Greg Johnston
36272a0b1b Revert "fix: constrain reactive rendering to Fn(), because using dry_resolve() for Suspense requires idempotent render functions so that they can be called once (to register resources) and called a second time to resolve"
This reverts commit 7ec5c77ba3e8f45bae04a7661a56741f95125adb.
2024-08-01 19:42:20 -04:00
Greg Johnston
96c956efdf progress on updating suspense tests 2024-08-01 19:42:20 -04:00
Greg Johnston
29cf1f4814 add server redirects 2024-08-01 19:42:20 -04:00
Greg Johnston
39c3a63787 fix: relative path resolution 2024-08-01 19:42:20 -04:00
Greg Johnston
068865b7de simplifying todo examples 2024-08-01 19:42:20 -04:00
Greg Johnston
fa8bb15a67 initial work updating suspense tests 2024-08-01 19:42:20 -04:00
Greg Johnston
faa481f2b6 clarify hydrate/csr warning 2024-08-01 19:42:20 -04:00
Greg Johnston
b41d988865 export actions in prelude 2024-08-01 19:42:20 -04:00
Greg Johnston
025c28b489 remove Into<_> by default for setting signals, because it interferes with type inference 2024-08-01 19:42:20 -04:00
Greg Johnston
0c7c7c9b38 add support for unsync actions 2024-08-01 19:42:20 -04:00
Greg Johnston
b109c3e9a3 simplifying and updating server fns example 2024-08-01 19:42:20 -04:00
Greg Johnston
0a559935e7 change name to shell 2024-08-01 19:42:20 -04:00
Greg Johnston
bccc05fec8 update control flow components to new Fn() constraint 2024-08-01 19:42:20 -04:00
Greg Johnston
e0f98dc0fd fix: constrain reactive rendering to Fn(), because using dry_resolve() for Suspense requires idempotent render functions so that they can be called once (to register resources) and called a second time to resolve 2024-08-01 19:42:20 -04:00
Greg Johnston
5d9bd8f913 add Debug impl 2024-08-01 19:42:20 -04:00
Greg Johnston
0a41ae9a5e fix: actually concatenate nested routes during route generation 2024-08-01 19:42:20 -04:00
Greg Johnston
fbc6be922d reorganize Outlet export 2024-08-01 19:42:20 -04:00
Greg Johnston
b5551863fe examples: porting to 0.7 and cleaning up 2024-08-01 19:42:20 -04:00
Greg Johnston
14b3877293 fix merge 2024-08-01 19:41:56 -04:00
Rakshith Ravi
98ea18009d Update import statements in examples (#2625) 2024-08-01 19:41:56 -04:00
Greg Johnston
d133cff092 examples: use application 404 page 2024-08-01 19:41:56 -04:00
Greg Johnston
48028b476a chore: cargo fmt 2024-08-01 19:41:56 -04:00
Greg Johnston
404ad50bd3 chore: cargo fmt 2024-08-01 19:41:56 -04:00
Greg Johnston
b89fbe027b add warnings if correct features not set for browser 2024-08-01 19:41:56 -04:00
Greg Johnston
0ba53afa08 use csr feature so that reactivity runs 2024-08-01 19:41:55 -04:00
Greg Johnston
c384b53a0f chore: clippy 2024-08-01 19:41:55 -04:00
Greg Johnston
2f53e09bb6 examples: fix input type so tests work, and update text to make the purpose clearer 2024-08-01 19:41:55 -04:00
Greg Johnston
319eefb169 remove leptos_reactive (moved into reactive_graph and leptos_server) 2024-08-01 19:41:55 -04:00
Greg Johnston
949f43d145 fix: Clone for ArcResource and default to SerdeJson for Resource 2024-08-01 19:41:55 -04:00
Greg Johnston
a47759007f chore: clippy 2024-08-01 19:41:55 -04:00
Greg Johnston
095faf15b1 fix: don't dispose of parent owners before Suspense children have been rendered 2024-08-01 19:41:55 -04:00
Greg Johnston
f9eb562050 warn if trying to use meta on server side without context 2024-08-01 19:41:55 -04:00
Greg Johnston
7f57b88e8d only run RenderEffects when effects are enabled 2024-08-01 19:41:55 -04:00
Greg Johnston
8a8862be9e add set_pending to <Transition/> 2024-08-01 19:41:55 -04:00
Greg Johnston
619dc59e1d simplify FlatRoutes logic by using existing OwnedView infrastructure 2024-08-01 19:41:55 -04:00
Greg Johnston
5f49504137 reexport tick() for testing 2024-08-01 19:41:55 -04:00
Greg Johnston
ca68fa5a3d fix: ensure that leptos_meta and leptos_router are in SSR mode if using one of the server integrations 2024-08-01 19:41:55 -04:00
Greg Johnston
e6a472b467 examples: update hackernews for SSR support 2024-08-01 19:41:55 -04:00
Greg Johnston
f8da9e30e0 fix: correctly notify multiple subscribers to same AsyncDerived 2024-08-01 19:41:36 -04:00
Greg Johnston
984ede8887 fix: Routes SSR 2024-08-01 19:41:36 -04:00
Greg Johnston
c3656416a2 fix: correct owner for HTML rendering in FlatRoutes 2024-08-01 19:41:36 -04:00
Greg Johnston
7ecfbd9109 testing: provide tick() that can be called anywhere in tests 2024-08-01 19:41:36 -04:00
Greg Johnston
531c39759a testing: provide tick() that can be called anywhere in tests 2024-08-01 19:41:36 -04:00
Greg Johnston
f5d06577f4 fix portal tests 2024-08-01 19:41:36 -04:00
Greg Johnston
39902d1e66 fix cleanups in render effects 2024-08-01 19:41:36 -04:00
Greg Johnston
7def5f65ed chore: clippy 2024-08-01 19:41:36 -04:00
Greg Johnston
6b60d48203 update counters_isomorphic 2024-08-01 19:41:36 -04:00
Greg Johnston
9ef51166d3 reexport spawn and spawn_local 2024-08-01 19:41:27 -04:00
Greg Johnston
8da6bbc3be ReadSignal from stream 2024-08-01 19:41:27 -04:00
Greg Johnston
3c39674622 refactor to allow rendering Resource directly in view 2024-08-01 19:41:27 -04:00
Greg Johnston
914b07491e removed AnimatedShow example (duplicates the component docs) 2024-08-01 19:41:27 -04:00
Greg Johnston
1d2d11b83d properly serialize errors 2024-08-01 19:41:27 -04:00
Greg Johnston
07e878adf7 chore: clear warning 2024-08-01 19:41:27 -04:00
Greg Johnston
f32d43ce94 pick up on server action error in both server and client 2024-08-01 19:41:27 -04:00
Greg Johnston
65e3c57ed1 fmt and chores in examples 2024-08-01 19:41:27 -04:00
Greg Johnston
2e40bace88 fix: serialize an empty string into HTML so it still works as a text node 2024-08-01 19:41:27 -04:00
Greg Johnston
b9945e0ce1 fix: make router fallback lazy 2024-08-01 19:41:27 -04:00
Greg Johnston
d7f70214b9 add expect_context 2024-08-01 19:41:27 -04:00
Greg Johnston
adf57f5771 fix attr:class when spreading onto a component 2024-08-01 19:41:27 -04:00
Greg Johnston
bae79e2b2c add ServerAction error handling for any error type (closes #2325) 2024-08-01 19:41:27 -04:00
Greg Johnston
e2b1210461 remove unused import 2024-08-01 19:41:13 -04:00
Greg Johnston
7c24b7482d clean up example 2024-08-01 19:41:13 -04:00
Greg Johnston
25c66a4624 add CollectView 2024-08-01 19:41:13 -04:00
Greg Johnston
71ddacef8e Actix todo_app_sqlite 2024-08-01 19:41:13 -04:00
Greg Johnston
338b01bee3 fix: don't drop Owner in FlatRoutes until route has been rendered (thanks @benwis) 2024-08-01 19:41:13 -04:00
Greg Johnston
a36f22e439 fix: make sure all resource reads are registered 2024-08-01 19:41:13 -04:00
Greg Johnston
56977411f2 chore: clippy and unused dependencies in integrations 2024-08-01 19:41:13 -04:00
Greg Johnston
0fc47e3a35 add some tracing and debug info to HTML elements 2024-08-01 19:41:13 -04:00
Greg Johnston
caf797dba0 refactor integrations and add Actix integration 2024-08-01 19:41:13 -04:00
Greg Johnston
2f54d937a1 feat: 0.7 nonce support 2024-08-01 19:41:13 -04:00
Greg Johnston
40c1f38a07 ResponseOptions support 2024-08-01 19:41:13 -04:00
Greg Johnston
402d6297f4 fix counters tests 2024-08-01 19:41:13 -04:00
Greg Johnston
93734a5222 allow .children() on HTML elements 2024-08-01 19:41:13 -04:00
Greg Johnston
770d02d8e6 remove async demo 2024-08-01 19:41:13 -04:00
Greg Johnston
e275862a20 fix: writing to lock that has a read 2024-08-01 19:41:13 -04:00
Greg Johnston
17f1d25d03 allow untracking on write guards to support maybe_update 2024-08-01 19:41:13 -04:00
Greg Johnston
0a99a378aa feat: allow .write() on all writeable signals 2024-08-01 19:41:13 -04:00
Greg Johnston
14b7073863 feat: add .by_ref() to create a Future from an AsyncDerived (etc.) that takes a reference, rather than cloning 2024-08-01 19:41:12 -04:00
Greg Johnston
4e4deef144 use impl trait in props 2024-08-01 19:41:12 -04:00
Greg Johnston
c360f0ed0d update wasm-bindgen testing approaches 2024-08-01 19:41:12 -04:00
Greg Johnston
88ab9693db chore: clearing warnings in examples 2024-08-01 19:41:12 -04:00
Greg Johnston
6dfea0b0a2 additional warnings 2024-08-01 19:41:12 -04:00
Greg Johnston
9fd881603f cargo fmt 2024-08-01 19:41:12 -04:00
Greg Johnston
9666c9c0c5 chore: clear up... a few warnings 2024-08-01 19:41:12 -04:00
Greg Johnston
9e8b304b8a update sledgehammer integration 2024-08-01 19:41:12 -04:00
Greg Johnston
064ccce5b1 remove signal function setter Send-only implementation (dead code) 2024-08-01 19:41:12 -04:00
Greg Johnston
2e31177f62 remove signal function call Read implementations (dead code) 2024-08-01 19:41:12 -04:00
Greg Johnston
4215cef04b remove leptos_reactive dependency 2024-08-01 19:41:12 -04:00
Greg Johnston
de3dd3c296 oco merge issues 2024-08-01 19:41:12 -04:00
Greg Johnston
846ff2fefb feat: return an async guard from .await rather than cloning the value every time 2024-08-01 19:41:12 -04:00
Greg Johnston
6003212f6e fix return type in async tests 2024-08-01 19:41:11 -04:00
Greg Johnston
054cff7883 fix tests that run effects 2024-08-01 19:41:11 -04:00
Greg Johnston
ce5738d7c4 feat: return Option from AsyncDerived.get() instead of AsyncState 2024-08-01 19:41:11 -04:00
Greg Johnston
47331b5c8d example: restore ErrorBoundary 2024-08-01 19:41:11 -04:00
Greg Johnston
a6cee3b1e9 docs for Owner and context 2024-08-01 19:41:11 -04:00
Greg Johnston
43c0e384c4 fix tests 2024-08-01 19:41:11 -04:00
Greg Johnston
db654cbfda poll AsyncDerived synchronously so that it has the correct value during hydration if it reads from a resource 2024-08-01 19:41:11 -04:00
Greg Johnston
e13b1561d8 correct dirty-checking on AsyncDerived 2024-08-01 19:41:11 -04:00
Greg Johnston
02f76dec35 fix regular suspense if nothing was read synchronously 2024-08-01 19:41:11 -04:00
Greg Johnston
4bd99a41e5 missing dry_resolve on Static 2024-08-01 19:41:11 -04:00
Greg Johnston
85d29a5af5 feat: support *either* .await or reactive reads inside Suspense 2024-08-01 19:41:11 -04:00
Greg Johnston
4d54574f9e feat: 0.7 slots 2024-08-01 19:41:11 -04:00
Greg Johnston
f6c7ac473a feat: enhanced spreading syntax 2024-08-01 19:41:11 -04:00
Greg Johnston
747d847183 fix external navigations 2024-08-01 19:41:11 -04:00
Greg Johnston
8dd63a402b make WindowListenerHandle Send + Sync so it can be remove via on_cleanup 2024-08-01 19:41:11 -04:00
Greg Johnston
694eccbadc restore ssr/hydration for Routes 2024-08-01 19:41:11 -04:00
Greg Johnston
24f2e71563 get nested Routes working again 2024-08-01 19:41:11 -04:00
Greg Johnston
1766bfedb9 default to Params::get() giving an owned value (which you want in a derived signal), but use reference in the macro 2024-08-01 19:41:11 -04:00
Greg Johnston
242d35cc37 add proper dirty checking on AsyncDerived so it can read from memos properly 2024-08-01 19:41:11 -04:00
Greg Johnston
85b9f87620 make NavigateOptions pub 2024-08-01 19:41:11 -04:00
Greg Johnston
db33bc2e61 feat: owning memo 2024-08-01 19:41:11 -04:00
Greg Johnston
a1329ea044 remove warnings in tests and only run if effects are enabled 2024-08-01 19:41:11 -04:00
Greg Johnston
050bf8f821 fix: prevent memos that have changed from re-triggering the running effect, by setting the Observer during .update_if_necessary() 2024-08-01 19:41:11 -04:00
Greg Johnston
1a68743fcc feat: add Popover API 2024-08-01 19:41:11 -04:00
Greg Johnston
2925db8676 fix Script children 2024-08-01 19:41:11 -04:00
Greg Johnston
13d5f12d7f fix hydration of Suspend by including the missing placeholder it expects during hydration 2024-08-01 19:41:11 -04:00
Greg Johnston
3d9c295613 add missing marker comments for Result 2024-08-01 19:41:11 -04:00
Greg Johnston
b2c0068e2c include marker comments in html len 2024-08-01 19:41:11 -04:00
Greg Johnston
94a3f7c092 unused owner 2024-08-01 19:41:11 -04:00
Greg Johnston
330dcfeb7c impl From/Into for Signal/ArcSignal 2024-08-01 19:41:11 -04:00
Greg Johnston
f7bbec5f06 add ArcSignal::derive() 2024-08-01 19:41:11 -04:00
Greg Johnston
8815529955 routing progress indicator 2024-08-01 19:41:11 -04:00
Greg Johnston
12db58a7e0 missing min attribute 2024-08-01 19:41:11 -04:00
Greg Johnston
83c9edde26 clean up 2024-08-01 19:41:11 -04:00
Greg Johnston
2037a6966a remove log 2024-08-01 19:41:11 -04:00
Greg Johnston
4f041f5a5e relax trait bounds on reactive types where possible 2024-08-01 19:41:11 -04:00
Greg Johnston
6467e067ef add SignalSetter 2024-08-01 19:41:11 -04:00
Greg Johnston
3814879d80 use transition between navigations 2024-08-01 19:41:11 -04:00
Greg Johnston
5e16ae6a26 add async transitions that wait for any AsyncDerived created/triggered under them before resolving 2024-08-01 19:41:11 -04:00
Greg Johnston
6d474713f6 resolve() on OwnedView 2024-08-01 19:41:11 -04:00
Greg Johnston
0d47399424 restore hydration feature for some of its feature-gating benefits for Resource deserialization 2024-08-01 19:41:11 -04:00
Greg Johnston
ae254836d7 cargo fmt 2024-08-01 19:41:11 -04:00
Greg Johnston
2dd5efc5d0 create separate URL/params signals for each route, to prevent updating them and running side effects while navigating away 2024-08-01 19:41:11 -04:00
Greg Johnston
15eeda9c7a fmt 2024-08-01 19:41:11 -04:00
Greg Johnston
1a739015e1 distinguish between dirty and check in effects, so that memos and signals both work correctly 2024-08-01 19:41:11 -04:00
Greg Johnston
8385287123 remove unused feature 2024-08-01 19:41:11 -04:00
Greg Johnston
c4aa3ba1ba updated future impls 2024-08-01 19:41:11 -04:00
Greg Johnston
ce5f2c81ed check whether ArcAsyncDerived actually needs to run when marked check 2024-08-01 19:41:11 -04:00
Greg Johnston
941689fc5b add ancestry debugging for owners 2024-08-01 19:41:11 -04:00
Greg Johnston
961bf89a8b lazy Future construction for AsyncDerived 2024-08-01 19:41:11 -04:00
Greg Johnston
d360cc280f support Resource in CSR for backward-compat 2024-08-01 19:41:11 -04:00
Greg Johnston
bb7bb8f4c2 allow let: syntax to work 2024-08-01 19:41:11 -04:00
Greg Johnston
b29b8fb5ff scope Suspense/Transition correctly within ownership tree 2024-08-01 19:41:11 -04:00
Greg Johnston
4ffa3c46b6 upgrading hackernews example 2024-08-01 19:41:10 -04:00
Greg Johnston
32294d6cab immediately commit URL signal updates 2024-08-01 19:40:57 -04:00
Greg Johnston
46d286755e reexport A from router::components 2024-08-01 19:40:57 -04:00
Greg Johnston
b936e0352f add IntoAny to tachys prelude 2024-08-01 19:40:57 -04:00
Greg Johnston
b5bd70ab94 finish support for innerHTML 2024-08-01 19:40:57 -04:00
Greg Johnston
0dd1932b7f feat: iterating over items in children with ChildrenFragment, ChildrenFragmentFn, ChildrenFragmentMut 2024-08-01 19:40:57 -04:00
Greg Johnston
f5d203f0c9 only warn about non-reactive accesses if effects are enabled 2024-08-01 19:40:57 -04:00
Greg Johnston
1bc0b414e3 only run effects on client 2024-08-01 19:40:57 -04:00
Greg Johnston
d6e19c0a60 resolve() implementation for AnyView 2024-08-01 19:40:57 -04:00
Greg Johnston
fc60d6b2d7 fix deadlock on nested Signals 2024-08-01 19:40:57 -04:00
Greg Johnston
292e7c1f27 fix FlatRouter SSR/hydration after lazy routes 2024-08-01 19:40:57 -04:00
Greg Johnston
1da84db1aa feat: nested islands with context for 0.7 2024-08-01 19:40:57 -04:00
Greg Johnston
535e3e3880 fix: correct Send + Sync bounds for children 2024-08-01 19:40:57 -04:00
Greg Johnston
109244b28b feat: minimal island support in 0.7 2024-08-01 19:40:57 -04:00
Greg Johnston
fd048295a4 docs: full docs and doctests for Action/MultiAction 2024-08-01 19:40:57 -04:00
Greg Johnston
26cf4848db remove support for rendering guards directly, as they are !Send and holding onto them in State is also a bad idea 2024-08-01 19:40:57 -04:00
Greg Johnston
757a5c73c3 support nightly static values for style:key="value" 2024-08-01 19:40:57 -04:00
Greg Johnston
da496def16 revert to using .get() for function calls 2024-08-01 19:40:57 -04:00
Greg Johnston
3a755bd8c3 fix: only rerun effects if they have dirty ancestors (or it's the first run) 2024-08-01 19:40:57 -04:00
Greg Johnston
e514f7144d start working on porting over docs and tests and 0.7... 2024-08-01 19:40:57 -04:00
Greg Johnston
b881167b8f fix meta issue with attributes 2024-08-01 19:40:57 -04:00
Greg Johnston
1e9d345831 fix tracing issue 2024-08-01 19:40:57 -04:00
Greg Johnston
7f7bba6ea3 chore: get tests in a working state 2024-08-01 19:40:57 -04:00
Greg Johnston
015a4b63ec fix: make Selector Send/Sync 2024-08-01 19:40:57 -04:00
Greg Johnston
dcec7af4f3 docs: runtime warning if you use .track() outside a tracking context 2024-08-01 19:40:57 -04:00
Greg Johnston
5bc97654dc preliminary tracing for tachys 2024-08-01 19:40:57 -04:00
Greg Johnston
2788d93e96 chore: warnings 2024-08-01 19:40:57 -04:00
Greg Johnston
604043b4d8 examples: router in 0.7 2024-08-01 19:40:57 -04:00
Greg Johnston
ab28c80593 fix: passing context through router 2024-08-01 19:40:57 -04:00
Greg Johnston
49da073fed chore: fix warnings about variable case 2024-08-01 19:40:57 -04:00
Greg Johnston
3629302f88 examples: timer in 0.7 2024-08-01 19:40:57 -04:00
Greg Johnston
274e31018b feat: Portals in 0.7 2024-08-01 19:40:57 -04:00
Greg Johnston
802fcc5c2a allow either eager or lazy routes 2024-08-01 19:40:57 -04:00
Greg Johnston
da084a2ece update StoredValue API in callbacks 2024-08-01 19:40:57 -04:00
Greg Johnston
d9f6836933 chore: clippy warnings 2024-08-01 19:40:57 -04:00
Greg Johnston
d8d2fdac5d smooth out StoredValue APIs 2024-08-01 19:40:57 -04:00
Greg Johnston
9818e7cb68 MaybeSignal and MaybeProp 2024-08-01 19:40:57 -04:00
Marc-Stefan Cassola
986fbe5328 added a few old deprecated functions to help users port (#2580) 2024-08-01 19:40:57 -04:00
Greg Johnston
711175a760 implement With(Untracked) for Signal 2024-08-01 19:40:57 -04:00
Greg Johnston
00a536a5dc don't over-rerender nested router 2024-08-01 19:40:57 -04:00
Greg Johnston
a7b1152910 initial async routing work (to support bundle splitting) 2024-08-01 19:40:57 -04:00
Greg Johnston
cfba7a2797 noop attribute 'spreading' for routers 2024-08-01 19:40:57 -04:00
Greg Johnston
cebe744a84 support arbitrary attributes on components in view 2024-08-01 19:40:57 -04:00
Greg Johnston
e93a34a2c9 full attribute spreading 2024-08-01 19:40:57 -04:00
Greg Johnston
9ec30d71d2 update counter_without_macros imports 2024-08-01 19:40:57 -04:00
Greg Johnston
3c13280bf6 stashing 2024-08-01 19:40:57 -04:00
Greg Johnston
45fd9423f8 give a route to upgrade any attribute into a cloneable one 2024-08-01 19:40:56 -04:00
Greg Johnston
7a92208c4f work on attribute spreading 2024-08-01 19:40:56 -04:00
Greg Johnston
89b972e3c5 disable AddAnyAttr again now that I remember why it was broken 2024-08-01 19:40:56 -04:00
Greg Johnston
8dac92b251 reenable AnyAttr 2024-08-01 19:40:56 -04:00
Greg Johnston
b24eaedfe9 reorganizing exports and updating examples 2024-08-01 19:40:56 -04:00
Greg Johnston
4336051f78 prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
97ce5adb8e fix reorganized exports 2024-08-01 19:40:56 -04:00
Greg Johnston
20fb5454b0 prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
aac607f338 prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
738986415d prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
3406446ebd module restructuring for 0.7 2024-08-01 19:40:56 -04:00
Greg Johnston
21dd7e9c76 let ErrorBoundary own the fallback 2024-08-01 19:40:56 -04:00
Greg Johnston
9bab4da172 make Suspend a transparent wrapper 2024-08-01 19:40:56 -04:00
Greg Johnston
420dccda60 provide params properly in FlatRouter 2024-08-01 19:40:56 -04:00
Greg Johnston
53b22a9b74 clear some warnings 2024-08-01 19:40:56 -04:00
Greg Johnston
e68730d15f rename TupleBuilder to NextTuple and prep for release 2024-08-01 19:40:56 -04:00
Greg Johnston
11d134c4ba prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
2239f04f6b prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
78e5a7ebc3 prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
0148d92f48 prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
ab67bea7ec rename any_error 2024-08-01 19:40:56 -04:00
Greg Johnston
0beef3b2e0 prep for preview release 2024-08-01 19:40:56 -04:00
Greg Johnston
81fc7e6ada remove twiggy file 2024-08-01 19:40:56 -04:00
Greg Johnston
75d6763f4e move router crates 2024-08-01 19:40:56 -04:00
Greg Johnston
da4d2cf538 comparison demo 2024-08-01 19:40:25 -04:00
Greg Johnston
2470637b0b SSR optimizations for binary size, and flat router 2024-08-01 19:40:25 -04:00
Greg Johnston
2934c295b5 work on Axum integration and on error boundaries 2024-08-01 19:40:25 -04:00
Greg Johnston
789eef914d stash 2024-08-01 19:40:25 -04:00
Greg Johnston
782cb93743 feat: add <A> 2024-08-01 19:40:25 -04:00
Greg Johnston
8642c563d8 add use_navigate and Redirect 2024-08-01 19:40:25 -04:00
Greg Johnston
755fbd3866 preliminary use_navigate work 2024-08-01 19:40:25 -04:00
Greg Johnston
d83471e02b fix fallback => match update 2024-08-01 19:40:25 -04:00
Greg Johnston
2dd2bb5958 add more hooks and primitives to router 2024-08-01 19:40:25 -04:00
Greg Johnston
12f2cec5c7 nested route CSR working 2024-08-01 19:40:25 -04:00
Greg Johnston
a41bf2784f continuing on nested routes 2024-08-01 19:40:25 -04:00
Greg Johnston
ebdd31cd9f continuing on nested routes 2024-08-01 19:40:25 -04:00
Greg Johnston
acec3bb313 working on reconfiguring nested routing 2024-08-01 19:40:25 -04:00
Greg Johnston
464f157186 make placeholder-finding code consistent across container types 2024-08-01 19:40:25 -04:00
Greg Johnston
b53e4d8ff8 remove logs 2024-08-01 19:40:25 -04:00
Greg Johnston
cd438e0bcf fix Transition hydration 2024-08-01 19:40:25 -04:00
Greg Johnston
13da1e743d remove TryCatch/fallible rendering in favor of better ErrorBoundary model 2024-08-01 19:40:25 -04:00
Greg Johnston
0c9167fd30 finish todo_app_sqlite_axum 2024-08-01 19:40:25 -04:00
Greg Johnston
52da0e43ac fix Vec hydration 2024-08-01 19:40:25 -04:00
Greg Johnston
dad91f5960 add MultiActionForm 2024-08-01 19:40:25 -04:00
Greg Johnston
72e97047a5 add MultiAction/ServerMultiAction 2024-08-01 19:40:25 -04:00
Greg Johnston
883fd57fe1 stash 2024-08-01 19:40:25 -04:00
Greg Johnston
42b99dd912 ErrorBoundary SSR and serialization of errors to support hydration 2024-08-01 19:40:25 -04:00
Greg Johnston
851e1f73fd get types working with nested ErrorBoundary/Suspense 2024-08-01 19:40:25 -04:00
Greg Johnston
e11eea1af1 probably as far as I can go with the current SuspenseBoundary approach 2024-08-01 19:40:25 -04:00
Greg Johnston
f508cc4510 fix static types 2024-08-01 19:40:25 -04:00
Greg Johnston
e4f3cf9cca fix cancellation logic for server fn requests 2024-08-01 19:40:25 -04:00
Greg Johnston
60d883a26c only subscribe to memo manually if already loaded 2024-08-01 19:40:25 -04:00
Greg Johnston
add3be0ff5 Suspense SSR 2024-08-01 19:40:25 -04:00
Greg Johnston
a01640cafd updates toward todo_app_sqlite 2024-08-01 19:40:25 -04:00
Greg Johnston
e837e9fded fix stable examples 2024-08-01 19:40:25 -04:00
Greg Johnston
e0e67360aa implement rendering traits for signals directly on stable 2024-08-01 19:40:25 -04:00
Greg Johnston
439deea066 suspend!() macro 2024-08-01 19:40:25 -04:00
Greg Johnston
e5f5710f46 add Transition 2024-08-01 19:40:25 -04:00
Greg Johnston
8626db27d7 loosen requirements for Show 2024-08-01 19:40:25 -04:00
Greg Johnston
ec3f0933fe working on examples 2024-08-01 19:40:25 -04:00
Greg Johnston
b50de3a005 finish TodoMVC example 2024-08-01 19:40:25 -04:00
Greg Johnston
aa878534ad simplify Suspense: this should still work with hydration 2024-08-01 19:40:25 -04:00
Greg Johnston
603f9f96c4 working model for Suspense with new version 2024-08-01 19:40:25 -04:00
Greg Johnston
f78e675506 probably as far as I can go with the current SuspenseBoundary approach 2024-08-01 19:40:24 -04:00
Greg Johnston
cc2714c03d fix ErrorBoundary/Suspense 2024-08-01 19:40:24 -04:00
Greg Johnston
c06110128b feat: ErrorBoundary and Suspense 2024-08-01 19:40:24 -04:00
Greg Johnston
d7c62622ae feat: ErrorBoundary 2024-08-01 19:40:24 -04:00
Greg Johnston
1edec6c36a fix Cargo.toml merge issues 2024-08-01 19:40:24 -04:00
Greg Johnston
c5049ca1bb working on examples 2024-08-01 19:40:24 -04:00
Greg Johnston
f69dbb48ca styling with CSS 2024-08-01 19:40:24 -04:00
Greg Johnston
5feaf1aea6 example with isomorphic GTK/web design system 2024-08-01 19:40:24 -04:00
Greg Johnston
ec3ab6a355 gtk example 2024-08-01 19:40:24 -04:00
Greg Johnston
100ed7d926 ErrorBoundary component 2024-08-01 19:40:24 -04:00
Greg Johnston
88b93f40f9 Suspense/Transition components 2024-08-01 19:40:24 -04:00
Greg Johnston
b8b77138ea GTK example for 0.7 2024-08-01 19:40:24 -04:00
Greg Johnston
20c29cab89 add serde-wasm-bindgen encoding for resources 2024-08-01 19:40:24 -04:00
Greg Johnston
54fd74839a add typed children 2024-08-01 19:40:24 -04:00
Greg Johnston
ea3790d91c scope Arena to each request 2024-08-01 19:40:24 -04:00
Greg Johnston
f5935c6333 correctly omit HTML-generating code from AnyView 2024-08-01 19:40:24 -04:00
Greg Johnston
c8e5e1b16b experimental sledgehammer Renderer backend 2024-08-01 19:40:24 -04:00
Greg Johnston
a12c707f3f fix async context issues, add flat routing 2024-08-01 19:40:24 -04:00
Greg Johnston
6d9906111d test more dynamic string length work 2024-08-01 19:40:24 -04:00
Greg Johnston
5ea314c998 attribute value escaping 2024-08-01 19:40:24 -04:00
Greg Johnston
2bc04444e1 work related to 0.7 blog port 2024-08-01 19:40:24 -04:00
Greg Johnston
b41fde3ff9 work related to 0.7 blog port 2024-08-01 19:40:24 -04:00
Greg Johnston
c29081b12a completing work on meta 2024-08-01 19:40:24 -04:00
Greg Johnston
2fefc8b4bf completing work on meta 2024-08-01 19:40:24 -04:00
Greg Johnston
72b43d1e2b initial work on meta 2024-08-01 19:40:24 -04:00
Greg Johnston
39607adc94 initial work on meta 2024-08-01 19:40:24 -04:00
Greg Johnston
30c1cd921b stash 2024-08-01 19:40:24 -04:00
Greg Johnston
abfe3cabd2 fix nested route rebuilding 2024-08-01 19:40:24 -04:00
Greg Johnston
16bd2942db navigation between nested routes 2024-08-01 19:40:24 -04:00
Greg Johnston
13cccced06 initial stage for working nested route rendering 2024-08-01 19:40:24 -04:00
Greg Johnston
db4c1cb4b3 stash 2024-08-01 19:40:24 -04:00
Greg Johnston
9cdd8cac15 stash 2024-08-01 19:40:24 -04:00
Greg Johnston
84ebdc1b92 get basic routing working 2024-08-01 19:40:24 -04:00
Greg Johnston
9f02cc8cc1 stash 2024-08-01 19:40:24 -04:00
Greg Johnston
c3b9932172 reorganize 2024-08-01 19:40:24 -04:00
Greg Johnston
dbd9951a85 working on nesting routing 2024-08-01 19:40:24 -04:00
Greg Johnston
6eb8b44fff reorganize 2024-08-01 19:40:24 -04:00
Greg Johnston
4fa31be5dc stash 2024-08-01 19:40:24 -04:00
Greg Johnston
b46dffb729 abstract interface to walk nested routes and to access views 2024-08-01 19:40:24 -04:00
Greg Johnston
ca54762806 reorganize and clean up 2024-08-01 19:40:24 -04:00
Greg Johnston
f122f9109f nested route matching 2024-08-01 19:40:24 -04:00
Greg Johnston
f894d6e4f6 stash 2024-08-01 19:40:24 -04:00
Greg Johnston
4cc925c950 stash 2024-08-01 19:40:24 -04:00
Greg Johnston
21e53042e8 work on routing utils 2024-08-01 19:40:24 -04:00
Greg Johnston
4d3fb37b35 nested route matching working 2024-08-01 19:40:24 -04:00
Greg Johnston
d3a21c922d stash 2024-08-01 19:40:24 -04:00
Greg Johnston
317f90e1e3 use either_of crate 2024-08-01 19:40:24 -04:00
Greg Johnston
26869a78a0 nested routes take 1 2024-08-01 19:40:24 -04:00
Greg Johnston
f46f864f05 split EitherOfX into its own crate 2024-08-01 19:40:24 -04:00
Greg Johnston
b21f1853c6 work on routing 2024-08-01 19:40:24 -04:00
Greg Johnston
1454c5d272 work on routing 2024-08-01 19:40:24 -04:00
Greg Johnston
c1f4616a31 set up routing 2024-08-01 19:40:24 -04:00
Greg Johnston
a3c3478831 clear warning 2024-08-01 19:40:24 -04:00
Greg Johnston
1ca8a9189c chore: clear warnings 2024-08-01 19:40:24 -04:00
Greg Johnston
9e276a8879 pass on: to components (and lay basis for passing all other attributes) 2024-08-01 19:40:24 -04:00
Greg Johnston
53703f208a working on AddAttr 2024-08-01 19:40:24 -04:00
Greg Johnston
9a60b21a0a remove boilerplate: require that Node, Element, etc. types always be Clone + 'static 2024-08-01 19:40:24 -04:00
Greg Johnston
524ed395fa parent_child example 2024-08-01 19:40:24 -04:00
Greg Johnston
5bc8c4e0d3 use AnyError for all try_ rendering errors, so that they can compose 2024-08-01 19:40:24 -04:00
Greg Johnston
7f7003f7f1 support for guards with class: syntax 2024-08-01 19:40:24 -04:00
Greg Johnston
ddf2ac0cf7 add Borrow implementation to make it easier to abstract over T and Guard<T> 2024-08-01 19:40:24 -04:00
Greg Johnston
992e2bce78 finish error boundary (fix last state transition issue) 2024-08-01 19:40:24 -04:00
Greg Johnston
6c2469ec3a progress on error boundary that works with nested reactivity 2024-08-01 19:40:24 -04:00
Greg Johnston
a7162d7907 progress on error boundary that works with nested reactivity 2024-08-01 19:40:24 -04:00
Greg Johnston
f584154156 error example 2024-08-01 19:40:24 -04:00
Greg Johnston
13464b10c9 enable event delegation 2024-08-01 19:40:24 -04:00
Greg Johnston
696bf14d13 fix release build 2024-08-01 19:40:24 -04:00
Greg Johnston
be92bab3e5 update TODO.md 2024-08-01 19:40:24 -04:00
Greg Johnston
4bb2bc4797 store effects in reactive system 2024-08-01 19:40:24 -04:00
Greg Johnston
a8adf8eea2 todomvc example 2024-08-01 19:40:24 -04:00
Greg Johnston
1a7da39fb7 work on async demo 2024-08-01 19:40:23 -04:00
Greg Johnston
201adb7406 clone values for Futures 2024-08-01 19:40:23 -04:00
Greg Johnston
4df42cbc60 make guard types more nestable/flexible so that we can implement render traits on any of them 2024-08-01 19:40:23 -04:00
Greg Johnston
44a0a0a93a work on async demo 2024-08-01 19:40:23 -04:00
Greg Johnston
66e1e6d7a1 work on async demo 2024-08-01 19:40:23 -04:00
Greg Johnston
8252c4a977 feat: create generic any_spawner crate to share between reactive system and renderer 2024-08-01 19:40:23 -04:00
Greg Johnston
6a2eafcbc6 add other methods on Stored 2024-08-01 19:40:23 -04:00
Greg Johnston
b49a13f8c1 work on async demo 2024-08-01 19:40:23 -04:00
Greg Johnston
0d5c67408f stash: working on jsfb 2024-08-01 19:40:23 -04:00
Greg Johnston
1eddd5a5f1 chore: remove unnecessary log 2024-08-01 19:40:23 -04:00
Greg Johnston
ca1e62c0b9 fix: correct owner for rows of For, correct cleanup of arenas 2024-08-01 19:40:23 -04:00
Greg Johnston
043cd7dc61 fix: close memory leak in tasks waiting on channels 2024-08-01 19:40:23 -04:00
Greg Johnston
68486cfb72 feat: typed event targets 2024-08-01 19:40:23 -04:00
Greg Johnston
eea971b9fe working on examples 2024-08-01 19:40:23 -04:00
Greg Johnston
d726b56b71 begin migrating to leptos and leptos_dom packages 2024-08-01 19:40:23 -04:00
Greg Johnston
0fddfb4823 stash 2024-08-01 19:40:23 -04:00
Greg Johnston
17732a6e6a stash 2024-08-01 19:40:23 -04:00
Greg Johnston
c8441f0f00 chore: remove leptos_reactive and add reactive_graph 2024-08-01 19:40:23 -04:00
Greg Johnston
ff4cde0764 feat: improved ergonomics of read guards 2024-08-01 19:40:23 -04:00
Greg Johnston
1d38439bd8 feat: add Readable implementation for all types 2024-08-01 19:40:23 -04:00
Greg Johnston
9ca1cba504 feat: add no_std support in appropriate crates 2024-08-01 19:40:23 -04:00
Greg Johnston
63dacdcc95 feat: tachys 2024-08-01 19:40:23 -04:00
Greg Johnston
61f5294f67 feat: add Fn traits 2024-08-01 19:40:23 -04:00
Greg Johnston
0149632a4c docs: note re: execution order (see #2261 and #2262) 2024-08-01 19:40:23 -04:00
Greg Johnston
96384ed116 feat: modular SharedContext for hydration 2024-08-01 19:40:23 -04:00
Greg Johnston
f56023bb25 chore: split OrPoisoned trait into its own crate for reuse 2024-08-01 19:40:23 -04:00
Greg Johnston
6bb5d58369 feat: modular, trait-based, Send/Sync reactive system 2024-08-01 19:40:23 -04:00
Saber Haj Rabiee
d50012f8d4 chore: update gloo-net and reqwest to http 1.0 (closes #2688) (leptos 0.6) (#2751) 2024-08-01 19:39:54 -04:00
Greg Johnston
c9d4ea9307 Merge pull request #2755 from leptos-rs/nightly-july24
chore(ci): update nightly
2024-08-01 15:30:22 -04:00
Greg Johnston
77c74bccbb chore: cargo fmt 2024-08-01 14:53:24 -04:00
Greg Johnston
528d1eae65 chore(ci): update nightly 2024-08-01 14:48:52 -04:00
martin frances
5809c8f699 As of rust1.80: cargo clippy now reports doc indentation issues. (#2728) 2024-07-30 09:25:53 -07:00
renshuncui
b9c620d4cd chore: fix some comments (#2712)
Signed-off-by: renshuncui <renshun@111.com>
2024-07-29 09:30:50 -04:00
Greg Johnston
8c9dfd9c9d fix: untrack children in Portal to avoid re-triggering it accidentally (closes #2693) (#2713) 2024-07-29 09:29:18 -04:00
Greg Johnston
8848eb8b87 0.6.13 2024-07-24 08:00:11 -04:00
158 changed files with 3404 additions and 12158 deletions

View File

@@ -15,7 +15,7 @@ jobs:
test:
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
name: Run semver check (nightly-2024-04-14)
name: Run semver check (nightly-2024-08-01)
runs-on: ubuntu-latest
steps:
@@ -25,4 +25,4 @@ jobs:
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2
with:
rust-toolchain: nightly-2024-04-14
rust-toolchain: nightly-2024-08-01

View File

@@ -49,4 +49,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly-2024-04-14
toolchain: nightly-2024-08-01

View File

@@ -40,34 +40,35 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.7.0-alpha"
version = "0.7.0-beta"
rust-version = "1.75"
[workspace.dependencies]
throw_error = { path = "./any_error/", version = "0.1" }
throw_error = { path = "./any_error/", version = "0.2.0-beta" }
any_spawner = { path = "./any_spawner/", version = "0.1" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1" }
hydration_context = { path = "./hydration_context", version = "0.2.0-alpha" }
leptos = { path = "./leptos", version = "0.7.0-alpha" }
leptos_config = { path = "./leptos_config", version = "0.7.0-alpha" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-alpha" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-alpha" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-alpha" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-alpha" }
leptos_router = { path = "./router", version = "0.7.0-alpha" }
leptos_server = { path = "./leptos_server", version = "0.7.0-alpha" }
leptos_meta = { path = "./meta", version = "0.7.0-alpha" }
next_tuple = { path = "./next_tuple", version = "0.1.0-alpha" }
hydration_context = { path = "./hydration_context", version = "0.2.0-beta" }
leptos = { path = "./leptos", version = "0.7.0-beta" }
leptos_config = { path = "./leptos_config", version = "0.7.0-beta" }
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta" }
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta" }
leptos_router = { path = "./router", version = "0.7.0-beta" }
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta" }
leptos_server = { path = "./leptos_server", version = "0.7.0-beta" }
leptos_meta = { path = "./meta", version = "0.7.0-beta" }
next_tuple = { path = "./next_tuple", version = "0.1.0-beta" }
oco_ref = { path = "./oco", version = "0.2" }
or_poisoned = { path = "./or_poisoned", version = "0.1" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-alpha" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-alpha" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-alpha" }
server_fn = { path = "./server_fn", version = "0.7.0-alpha" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-alpha" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-alpha" }
tachys = { path = "./tachys", version = "0.1.0-alpha" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta" }
server_fn = { path = "./server_fn", version = "0.7.0-beta" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta" }
tachys = { path = "./tachys", version = "0.1.0-beta" }
[profile.release]
codegen-units = 1

View File

@@ -1,7 +1,7 @@
[package]
name = "throw_error"
edition = "2021"
version = "0.1.0"
version = "0.2.0-beta"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -9,7 +9,7 @@ use std::{
error,
fmt::{self, Display},
future::Future,
ops,
mem, ops,
pin::Pin,
sync::Arc,
task::{Context, Poll},
@@ -92,9 +92,25 @@ thread_local! {
static ERROR_HOOK: RefCell<Option<Arc<dyn ErrorHook>>> = RefCell::new(None);
}
/// Resets the error hook to its previous state when dropped.
pub struct ResetErrorHookOnDrop(Option<Arc<dyn ErrorHook>>);
impl Drop for ResetErrorHookOnDrop {
fn drop(&mut self) {
ERROR_HOOK.with_borrow_mut(|this| *this = self.0.take())
}
}
/// Returns the current error hook.
pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
ERROR_HOOK.with_borrow(Clone::clone)
}
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) {
ERROR_HOOK.with_borrow_mut(|this| *this = Some(hook))
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
ResetErrorHookOnDrop(
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
)
}
/// Invokes the error hook set by [`set_error_hook`] with the given error.
@@ -140,9 +156,10 @@ where
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
if let Some(hook) = &this.hook {
set_error_hook(Arc::clone(hook))
}
let _hook = this
.hook
.as_ref()
.map(|hook| set_error_hook(Arc::clone(hook)));
this.inner.poll(cx)
}
}

View File

@@ -23,7 +23,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
log = "0.4"
once_cell = "1.18"
gloo-net = { git = "https://github.com/rustwasm/gloo" }
gloo-net = { version = "0.6" }
wasm-bindgen = "0.2"
serde = { version = "1", features = ["derive"] }
simple_logger = "4.3"
@@ -33,12 +33,12 @@ send_wrapper = "0.6.0"
[features]
hydrate = ["leptos/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:tracing",
"leptos/ssr",
"leptos_actix",
"leptos_router/ssr",
"dep:actix-files",
"dep:actix-web",
"dep:tracing",
"leptos/ssr",
"leptos_actix",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]

View File

@@ -113,10 +113,10 @@ pub fn Counter() -> impl IntoView {
</p>
<ErrorBoundary fallback=|errors| move || format!("Error: {:#?}", errors.get())>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<button on:click=move |_| { clear.dispatch(()); }>"Clear"</button>
<button on:click=move |_| { dec.dispatch(()); }>"-1"</button>
<span>"Value: " <Suspense>{counter} "!"</Suspense></span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
<button on:click=move |_| { inc.dispatch(()); }>"+1"</button>
</div>
</ErrorBoundary>
</div>
@@ -224,12 +224,12 @@ pub fn MultiuserCounter() -> impl IntoView {
"This one uses server-sent events (SSE) to live-update when other users make changes."
</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<button on:click=move |_| { clear.dispatch(()); }>"Clear"</button>
<button on:click=move |_| { dec.dispatch(()); }>"-1"</button>
<span>
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
<button on:click=move |_| { inc.dispatch(()); }>"+1"</button>
</div>
</div>
}

View File

@@ -23,8 +23,8 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
log = "0.4"
serde = { version = "1", features = ["derive"] }
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
gloo-net = { version = "0.6", features = ["http"] }
reqwest = { version = "0.12", features = ["json"] }
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
send_wrapper = "0.6.0"
@@ -32,12 +32,7 @@ send_wrapper = "0.6.0"
[features]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
]
ssr = ["dep:actix-files", "dep:actix-web", "dep:leptos_actix", "leptos/ssr"]
[profile.wasm-release]
inherits = "release"

View File

@@ -18,8 +18,8 @@ leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.4", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
gloo-net = { version = "0.6", features = ["http"] }
reqwest = { version = "0.12", features = ["json"] }
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }

View File

@@ -12,16 +12,14 @@ lto = true
[dependencies]
console_error_panic_hook = "0.1"
leptos = { path = "../../leptos", features = [
"experimental-islands",
] }
leptos = { path = "../../leptos", features = ["experimental-islands"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router"}
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.4", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
gloo-net = { version = "0.6", features = ["http"] }
reqwest = { version = "0.12", features = ["json"] }
axum = { version = "0.7", optional = true, features = ["http2"] }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = [
@@ -34,7 +32,11 @@ http = { version = "1.0", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
lazy_static = "1.4.0"
rust-embed = { version = "8", features = ["axum", "mime_guess", "tokio"], optional = true }
rust-embed = { version = "8", features = [
"axum",
"mime_guess",
"tokio",
], optional = true }
mime_guess = { version = "2.0.4", optional = true }
[features]

View File

@@ -21,7 +21,7 @@ leptos_router = { path = "../../router" }
leptos_server = { path = "../../leptos_server", optional = true }
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.5", features = ["http"] }
gloo-net = { version = "0.6", features = ["http"] }
reqwest = { version = "0.12", features = ["json"] }
axum = { version = "0.7", default-features = false, optional = true }
tower = { version = "0.4", optional = true }

View File

@@ -21,11 +21,7 @@ actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", features = ["macros"], optional = true }
[features]
hydrate = [
"leptos/hydrate",
"dep:wasm-bindgen",
"dep:console_error_panic_hook",
]
hydrate = ["leptos/hydrate", "dep:wasm-bindgen", "dep:console_error_panic_hook"]
ssr = [
"leptos/ssr",
"leptos_meta/ssr",

View File

@@ -7,5 +7,5 @@ edition = "2021"
leptos = { path = "../../leptos", features = ["csr"] }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
gloo-net = { version = "0.5", features = ["http"] }
gloo-net = { version = "0.6", features = ["http"] }
console_error_panic_hook = { version = "0.1" }

View File

@@ -1,7 +1,7 @@
[package]
name = "hydration_context"
edition = "2021"
version = "0.2.0-alpha"
version = "0.2.0-beta"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -44,6 +44,12 @@ pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
/// from the server to the client.
pub struct SerializedDataId(usize);
impl From<SerializedDataId> for ErrorId {
fn from(value: SerializedDataId) -> Self {
value.0.into()
}
}
/// Information that will be shared between the server and the client.
pub trait SharedContext: Debug {
/// Whether the application is running in the browser.

View File

@@ -16,6 +16,7 @@ use actix_web::{
};
use futures::{stream::once, Stream, StreamExt};
use http::StatusCode;
use hydration_context::SsrSharedContext;
use leptos::{
context::{provide_context, use_context},
reactive_graph::{computed::ScopedFuture, owner::Owner},
@@ -301,8 +302,9 @@ pub fn handle_server_fns_with_context(
let additional_context = additional_context.clone();
let path = req.path();
let method = req.method();
if let Some(mut service) =
server_fn::actix::get_server_fn_service(path)
server_fn::actix::get_server_fn_service(path, method)
{
let owner = Owner::new();
owner
@@ -384,7 +386,7 @@ pub fn handle_server_fns_with_context(
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
@@ -406,11 +408,7 @@ pub fn handle_server_fns_with_context(
/// // the actual routing will be handled by `leptos_router`
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_app_to_stream(
/// leptos_options.to_owned(),
/// || view! { <MyApp/> },
/// Method::Get,
/// ),
/// leptos_actix::render_app_to_stream(MyApp, Method::Get),
/// )
/// })
/// .bind(&addr)?
@@ -452,7 +450,7 @@ where
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
@@ -475,8 +473,7 @@ where
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_app_to_stream_in_order(
/// leptos_options.to_owned(),
/// || view! { <MyApp/> },
/// MyApp,
/// Method::Get,
/// ),
/// )
@@ -518,7 +515,7 @@ where
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
@@ -540,11 +537,7 @@ where
/// // the actual routing will be handled by `leptos_router`
/// .route(
/// "/{tail:.*}",
/// leptos_actix::render_app_async(
/// leptos_options.to_owned(),
/// || view! { <MyApp/> },
/// Method::Get,
/// ),
/// leptos_actix::render_app_async(MyApp, Method::Get),
/// )
/// })
/// .bind(&addr)?
@@ -944,7 +937,7 @@ where
{
let _ = any_spawner::Executor::init_tokio();
let owner = Owner::new_root(None);
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
let (mock_meta, _) = ServerMetaContext::new();
let routes = owner
.with(|| {
@@ -1380,18 +1373,21 @@ impl LeptosRoutes for &mut ServiceConfig {
///
/// Any error that occurs during extraction is converted to a [`ServerFnError`].
///
/// ```rust,ignore
/// // MyQuery is some type that implements `Deserialize + Serialize`
/// ```rust
/// use leptos::prelude::*;
///
/// #[server]
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
/// use actix_web::web::Query;
/// pub async fn extract_connection_info() -> Result<String, ServerFnError> {
/// use actix_web::dev::ConnectionInfo;
/// use leptos_actix::*;
///
/// let Query(data) = extract().await?;
/// // this can be any type you can use an Actix extractor with, as long as
/// // it works on the head, not the body of the request
/// let info: ConnectionInfo = extract().await?;
///
/// // do something with the data
///
/// Ok(data)
/// Ok(format!("{info:?}"))
/// }
/// ```
pub async fn extract<T>() -> Result<T, ServerFnError>
@@ -1399,7 +1395,7 @@ where
T: actix_web::FromRequest,
<T as FromRequest>::Error: Display,
{
let req = use_context::<HttpRequest>().ok_or_else(|| {
let req = use_context::<Request>().ok_or_else(|| {
ServerFnError::new("HttpRequest should have been provided via context")
})?;

View File

@@ -1,148 +1,153 @@
use leptos::*;
use leptos_actix::generate_route_list;
use leptos_router::{Route, Router, Routes, TrailingSlash};
// TODO these tests relate to trailing-slash logic, which is still TBD for 0.7
#[component]
fn DefaultApp() -> impl IntoView {
let view = || view! { "" };
view! {
<Router>
<Routes>
<Route path="/foo" view/>
<Route path="/bar/" view/>
<Route path="/baz/:id" view/>
<Route path="/baz/:name/" view/>
<Route path="/baz/*any" view/>
</Routes>
</Router>
}
}
#[test]
fn test_default_app() {
let routes = generate_route_list(DefaultApp);
// We still have access to the original (albeit normalized) Leptos paths:
assert_same(
&routes,
|r| r.leptos_path(),
&["/bar", "/baz/*any", "/baz/:id", "/baz/:name", "/foo"],
);
// ... But leptos-actix has also reformatted "paths" to work for Actix.
assert_same(
&routes,
|r| r.path(),
&["/bar", "/baz/{id}", "/baz/{name}", "/baz/{tail:.*}", "/foo"],
);
}
#[component]
fn ExactApp() -> impl IntoView {
let view = || view! { "" };
let trailing_slash = TrailingSlash::Exact;
view! {
<Router trailing_slash>
<Routes>
<Route path="/foo" view/>
<Route path="/bar/" view/>
<Route path="/baz/:id" view/>
<Route path="/baz/:name/" view/>
<Route path="/baz/*any" view/>
</Routes>
</Router>
}
}
#[test]
fn test_exact_app() {
let routes = generate_route_list(ExactApp);
// In Exact mode, the Leptos paths no longer have their trailing slashes stripped:
assert_same(
&routes,
|r| r.leptos_path(),
&["/bar/", "/baz/*any", "/baz/:id", "/baz/:name/", "/foo"],
);
// Actix paths also have trailing slashes as a result:
assert_same(
&routes,
|r| r.path(),
&[
"/bar/",
"/baz/{id}",
"/baz/{name}/",
"/baz/{tail:.*}",
"/foo",
],
);
}
#[component]
fn RedirectApp() -> impl IntoView {
let view = || view! { "" };
let trailing_slash = TrailingSlash::Redirect;
view! {
<Router trailing_slash>
<Routes>
<Route path="/foo" view/>
<Route path="/bar/" view/>
<Route path="/baz/:id" view/>
<Route path="/baz/:name/" view/>
<Route path="/baz/*any" view/>
</Routes>
</Router>
}
}
#[test]
fn test_redirect_app() {
let routes = generate_route_list(RedirectApp);
assert_same(
&routes,
|r| r.leptos_path(),
&[
"/bar",
"/bar/",
"/baz/*any",
"/baz/:id",
"/baz/:id/",
"/baz/:name",
"/baz/:name/",
"/foo",
"/foo/",
],
);
// ... But leptos-actix has also reformatted "paths" to work for Actix.
assert_same(
&routes,
|r| r.path(),
&[
"/bar",
"/bar/",
"/baz/{id}",
"/baz/{id}/",
"/baz/{name}",
"/baz/{name}/",
"/baz/{tail:.*}",
"/foo",
"/foo/",
],
);
}
fn assert_same<'t, T, F, U>(
input: &'t Vec<T>,
mapper: F,
expected_sorted_values: &[U],
) where
F: Fn(&'t T) -> U + 't,
U: Ord + std::fmt::Debug,
{
let mut values: Vec<U> = input.iter().map(mapper).collect();
values.sort();
assert_eq!(values, expected_sorted_values);
}
// use leptos::*;
// use leptos_actix::generate_route_list;
// use leptos_router::{
// components::{Route, Router, Routes},
// path,
// };
//
// #[component]
// fn DefaultApp() -> impl IntoView {
// let view = || view! { "" };
// view! {
// <Router>
// <Routes>
// <Route path=path!("/foo") view/>
// <Route path=path!("/bar/") view/>
// <Route path=path!("/baz/:id") view/>
// <Route path=path!("/baz/:name/") view/>
// <Route path=path!("/baz/*any") view/>
// </Routes>
// </Router>
// }
// }
//
// #[test]
// fn test_default_app() {
// let routes = generate_route_list(DefaultApp);
//
// // We still have access to the original (albeit normalized) Leptos paths:
// assert_same(
// &routes,
// |r| r.leptos_path(),
// &["/bar", "/baz/*any", "/baz/:id", "/baz/:name", "/foo"],
// );
//
// // ... But leptos-actix has also reformatted "paths" to work for Actix.
// assert_same(
// &routes,
// |r| r.path(),
// &["/bar", "/baz/{id}", "/baz/{name}", "/baz/{tail:.*}", "/foo"],
// );
// }
//
// #[component]
// fn ExactApp() -> impl IntoView {
// let view = || view! { "" };
// //let trailing_slash = TrailingSlash::Exact;
// view! {
// <Router>
// <Routes>
// <Route path=path!("/foo") view/>
// <Route path=path!("/bar/") view/>
// <Route path=path!("/baz/:id") view/>
// <Route path=path!("/baz/:name/") view/>
// <Route path=path!("/baz/*any") view/>
// </Routes>
// </Router>
// }
// }
//
// #[test]
// fn test_exact_app() {
// let routes = generate_route_list(ExactApp);
//
// // In Exact mode, the Leptos paths no longer have their trailing slashes stripped:
// assert_same(
// &routes,
// |r| r.leptos_path(),
// &["/bar/", "/baz/*any", "/baz/:id", "/baz/:name/", "/foo"],
// );
//
// // Actix paths also have trailing slashes as a result:
// assert_same(
// &routes,
// |r| r.path(),
// &[
// "/bar/",
// "/baz/{id}",
// "/baz/{name}/",
// "/baz/{tail:.*}",
// "/foo",
// ],
// );
// }
//
// #[component]
// fn RedirectApp() -> impl IntoView {
// let view = || view! { "" };
// //let trailing_slash = TrailingSlash::Redirect;
// view! {
// <Router>
// <Routes>
// <Route path=path!("/foo") view/>
// <Route path=path!("/bar/") view/>
// <Route path=path!("/baz/:id") view/>
// <Route path=path!("/baz/:name/") view/>
// <Route path=path!("/baz/*any") view/>
// </Routes>
// </Router>
// }
// }
//
// #[test]
// fn test_redirect_app() {
// let routes = generate_route_list(RedirectApp);
//
// assert_same(
// &routes,
// |r| r.leptos_path(),
// &[
// "/bar",
// "/bar/",
// "/baz/*any",
// "/baz/:id",
// "/baz/:id/",
// "/baz/:name",
// "/baz/:name/",
// "/foo",
// "/foo/",
// ],
// );
//
// // ... But leptos-actix has also reformatted "paths" to work for Actix.
// assert_same(
// &routes,
// |r| r.path(),
// &[
// "/bar",
// "/bar/",
// "/baz/{id}",
// "/baz/{id}/",
// "/baz/{name}",
// "/baz/{name}/",
// "/baz/{tail:.*}",
// "/foo",
// "/foo/",
// ],
// );
// }
//
// fn assert_same<'t, T, F, U>(
// input: &'t Vec<T>,
// mapper: F,
// expected_sorted_values: &[U],
// ) where
// F: Fn(&'t T) -> U + 't,
// U: Ord + std::fmt::Debug,
// {
// let mut values: Vec<U> = input.iter().map(mapper).collect();
// values.sort();
// assert_eq!(values, expected_sorted_values);
// }

View File

@@ -32,7 +32,7 @@ tracing = "0.1"
[dev-dependencies]
axum = "0.7"
tokio = { version = "1", features = ["net"] }
tokio = { version = "1", features = ["net", "rt-multi-thread"] }
[features]
wasm = []

View File

@@ -34,16 +34,22 @@
use axum::{
body::{Body, Bytes},
extract::{FromRef, FromRequestParts, MatchedPath, State},
extract::{FromRequestParts, MatchedPath},
http::{
header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
request::Parts,
HeaderMap, Method, Request, Response, StatusCode, Uri,
HeaderMap, Method, Request, Response, StatusCode,
},
response::IntoResponse,
routing::{delete, get, patch, post, put},
};
#[cfg(feature = "default")]
use axum::{
extract::{FromRef, State},
http::Uri,
};
use futures::{stream::once, Future, Stream, StreamExt};
use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
@@ -62,6 +68,7 @@ use leptos_router::{
use parking_lot::RwLock;
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
#[cfg(feature = "default")]
use tower::ServiceExt;
#[cfg(feature = "default")]
use tower_http::services::ServeDir;
@@ -95,7 +102,9 @@ impl ResponseParts {
///
/// If you provide your own handler, you will need to provide `ResponseOptions` via context
/// yourself if you want to access it via context.
/// ```rust,ignore
/// ```
/// use leptos::prelude::*;
///
/// #[server]
/// pub async fn get_opts() -> Result<(), ServerFnError> {
/// let opts = expect_context::<leptos_axum::ResponseOptions>();
@@ -228,7 +237,7 @@ pub fn generate_request_and_parts(
///
/// ```
/// use axum::{handler::Handler, routing::post, Router};
/// use leptos::*;
/// use leptos::prelude::*;
/// use std::net::SocketAddr;
///
/// # if false { // don't actually try to run a server in a doctest...
@@ -312,10 +321,13 @@ async fn handle_server_fns_inner(
) -> impl IntoResponse {
use server_fn::middleware::Service;
let method = req.method().clone();
let path = req.uri().path().to_string();
let (req, parts) = generate_request_and_parts(req);
if let Some(mut service) = server_fn::axum::get_server_fn_service(&path) {
if let Some(mut service) =
server_fn::axum::get_server_fn_service(&path, method)
{
let owner = Owner::new();
owner
.with(|| {
@@ -388,8 +400,7 @@ pub type PinnedHtmlStream =
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::*;
/// use leptos_config::get_configuration;
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -407,8 +418,7 @@ pub type PinnedHtmlStream =
///
/// // build our application with a route
/// let app = Router::new().fallback(leptos_axum::render_app_to_stream(
/// leptos_options,
/// || view! { <MyApp/> },
/// || { /* your application here */ },
/// ));
///
/// // run our app with hyper
@@ -476,8 +486,7 @@ where
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::*;
/// use leptos_config::get_configuration;
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -494,11 +503,9 @@ where
/// let addr = leptos_options.site_addr.clone();
///
/// // build our application with a route
/// let app =
/// Router::new().fallback(leptos_axum::render_app_to_stream_in_order(
/// leptos_options,
/// || view! { <MyApp/> },
/// ));
/// let app = Router::new().fallback(
/// leptos_axum::render_app_to_stream_in_order(|| view! { <MyApp/> }),
/// );
///
/// // run our app with hyper
/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
@@ -536,14 +543,25 @@ where
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```ignore
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
/// let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
/// || {
/// provide_context(id.clone());
/// },
/// || view! { <TodoApp/> }
/// );
/// ```
/// use axum::{
/// body::Body,
/// extract::Path,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::{config::LeptosOptions, context::provide_context, prelude::*};
///
/// async fn custom_handler(
/// Path(id): Path<String>,
/// req: Request<Body>,
/// ) -> Response {
/// let handler = leptos_axum::render_app_to_stream_with_context(
/// move || {
/// provide_context(id.clone());
/// },
/// || { /* your app here */ },
/// );
/// handler(req).await.into_response()
/// }
/// ```
@@ -694,14 +712,25 @@ where
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```ignore
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
/// let handler = leptos_axum::render_app_to_stream_in_order_with_context((*options).clone(),
/// move || {
/// provide_context(id.clone());
/// },
/// || view! { <TodoApp/> }
/// );
/// ```
/// use axum::{
/// body::Body,
/// extract::Path,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
/// Path(id): Path<String>,
/// req: Request<Body>,
/// ) -> Response {
/// let handler = leptos_axum::render_app_to_stream_in_order_with_context(
/// move || {
/// provide_context(id.clone());
/// },
/// || { /* your application here */ },
/// );
/// handler(req).await.into_response()
/// }
/// ```
@@ -834,8 +863,7 @@ fn provide_contexts(
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::{handler::Handler, Router};
/// use leptos::*;
/// use leptos_config::get_configuration;
/// use leptos::{config::get_configuration, prelude::*};
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -852,10 +880,8 @@ fn provide_contexts(
/// let addr = leptos_options.site_addr.clone();
///
/// // build our application with a route
/// let app = Router::new().fallback(leptos_axum::render_app_async(
/// leptos_options,
/// || view! { <MyApp/> },
/// ));
/// let app = Router::new()
/// .fallback(leptos_axum::render_app_async(|| view! { <MyApp/> }));
///
/// // run our app with hyper
/// // `axum::Server` is a re-export of `hyper::Server`
@@ -896,14 +922,25 @@ where
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```ignore
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
/// let handler = leptos_axum::render_app_async_with_context((*options).clone(),
/// move || {
/// provide_context(id.clone());
/// },
/// || view! { <TodoApp/> }
/// );
/// ```
/// use axum::{
/// body::Body,
/// extract::Path,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
/// Path(id): Path<String>,
/// req: Request<Body>,
/// ) -> Response {
/// let handler = leptos_axum::render_app_async_with_context(
/// move || {
/// provide_context(id.clone());
/// },
/// || { /* your application here */ },
/// );
/// handler(req).await.into_response()
/// }
/// ```
@@ -950,14 +987,25 @@ where
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```ignore
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
/// let handler = leptos_axum::render_app_async_with_context((*options).clone(),
/// move || {
/// provide_context(id.clone());
/// },
/// || view! { <TodoApp/> }
/// );
/// ```
/// use axum::{
/// body::Body,
/// extract::Path,
/// response::{IntoResponse, Response},
/// };
/// use http::Request;
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
/// Path(id): Path<String>,
/// req: Request<Body>,
/// ) -> Response {
/// let handler = leptos_axum::render_app_async_with_context(
/// move || {
/// provide_context(id.clone());
/// },
/// || { /* your application here */ },
/// );
/// handler(req).await.into_response()
/// }
/// ```
@@ -1166,7 +1214,7 @@ where
{
init_executor();
let owner = Owner::new_root(None);
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
let routes = owner
.with(|| {
// stub out a path for now
@@ -1676,15 +1724,19 @@ where
///
/// Any error that occurs during extraction is converted to a [`ServerFnError`].
///
/// ```rust,ignore
/// // MyQuery is some type that implements `Deserialize + Serialize`
/// #[server]
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
/// use axum::{extract::Query, http::Method};
/// use leptos_axum::*;
/// let Query(query) = extract().await?;
/// ```rust
/// use leptos::prelude::*;
///
/// Ok(query)
/// #[server]
/// pub async fn request_method() -> Result<String, ServerFnError> {
/// use http::Method;
/// use leptos_axum::extract;
///
/// // you can extract anything that a regular Axum extractor can extract
/// // from the head (not from the body of the request)
/// let method: Method = extract().await?;
///
/// Ok(format!("{method:?}"))
/// }
/// ```
pub async fn extract<T>() -> Result<T, ServerFnError>
@@ -1702,18 +1754,6 @@ where
/// therefore be used in an extractor. The compiler can often infer this type.
///
/// Any error that occurs during extraction is converted to a [`ServerFnError`].
///
/// ```rust,ignore
/// // MyQuery is some type that implements `Deserialize + Serialize`
/// #[server]
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
/// use axum::{extract::Query, http::Method};
/// use leptos_axum::*;
/// let Query(query) = extract().await?;
///
/// Ok(query)
/// }
/// ```
pub async fn extract_with_state<T, S>(state: &S) -> Result<T, ServerFnError>
where
T: Sized + FromRequestParts<S>,

View File

@@ -68,21 +68,19 @@ ssr = [
"hydration",
"tachys/ssr",
]
nightly = ["leptos_dom/nightly", "leptos_macro/nightly", "tachys/nightly"]
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
rkyv = ["server_fn/rkyv"]
tracing = [
"reactive_graph/tracing",
"tachys/tracing",
] #, "leptos_macro/tracing", "leptos_dom/tracing"]
nonce = ["base64", "leptos_dom/nonce", "rand"]
nonce = ["base64", "rand"]
spin = ["leptos-spin-macro"]
experimental-islands = [
"leptos_dom/experimental-islands",
"leptos_macro/experimental-islands",
"dep:serde_json",
]
trace-component-props = [
"leptos_dom/trace-component-props",
"leptos_macro/trace-component-props",
]
delegation = ["tachys/delegation"]

View File

@@ -6,7 +6,8 @@
//! Callbacks can be created manually from any function or closure, but the easiest way
//! to create them is to use `#[prop(into)]]` when defining a component.
//! ```
//! # use leptos::*;
//! use leptos::prelude::*;
//!
//! #[component]
//! fn MyComponent(
//! #[prop(into)] render_number: Callback<i32, String>,
@@ -118,8 +119,7 @@ macro_rules! impl_from_fn {
};
}
// TODO
//impl_from_fn!(UnsyncCallback);
impl_from_fn!(UnsyncCallback);
#[cfg(feature = "nightly")]
impl<In, Out> FnOnce<(In,)> for UnsyncCallback<In, Out> {
@@ -144,13 +144,12 @@ impl<In, Out> Fn<(In,)> for UnsyncCallback<In, Out> {
}
}
// TODO update these docs to swap the two
/// Callbacks define a standard way to store functions and closures.
///
/// # Example
/// ```
/// # use leptos::*;
/// # use leptos::{Callable, Callback};
/// # use leptos::prelude::*;
/// # use leptos::callback::{Callable, Callback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: Callback<i32, String>,
@@ -246,75 +245,30 @@ where
#[cfg(test)]
mod tests {
use crate::{
callback::{Callback, UnsyncCallback},
create_runtime,
};
use crate::callback::{Callback, UnsyncCallback};
struct NoClone {}
#[test]
fn clone_callback() {
let rt = create_runtime();
let callback =
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback.clone();
rt.dispose();
}
#[test]
fn clone_sync_callback() {
let rt = create_runtime();
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
fn clone_unsync_callback() {
let callback =
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback.clone();
rt.dispose();
}
#[test]
fn callback_from() {
let rt = create_runtime();
let _callback: UnsyncCallback<(), String> = (|()| "test").into();
rt.dispose();
}
#[test]
fn callback_from_html() {
let rt = create_runtime();
use leptos::{
html::{AnyElement, HtmlElement},
prelude::*,
};
let _callback: UnsyncCallback<String, HtmlElement<AnyElement>> =
(|x: String| {
view! { <h1>{x}</h1> }
})
.into();
rt.dispose();
let _callback: Callback<(), String> = (|()| "test").into();
}
#[test]
fn sync_callback_from() {
let rt = create_runtime();
let _callback: Callback<(), String> = (|()| "test").into();
rt.dispose();
}
#[test]
fn sync_callback_from_html() {
use leptos::{
html::{AnyElement, HtmlElement},
prelude::*,
};
let rt = create_runtime();
let _callback: Callback<String, HtmlElement<AnyElement>> =
(|x: String| {
view! { <h1>{x}</h1> }
})
.into();
rt.dispose();
let _callback: UnsyncCallback<(), String> = (|()| "test").into();
}
}

View File

@@ -47,70 +47,44 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView<Dom> + Send>;
/// to know exactly what children type the component expects. This is used internally by the
/// `view!` macro implementation, and can also be used explicitly when using the builder syntax.
///
/// # Examples
///
/// ## Without ToChildren
/// Different component types take different types for their `children` prop, some of which cannot
/// be directly constructed. Using `ToChildren` allows the component user to pass children without
/// explicity constructing the correct type.
///
/// Without [ToChildren], consumers need to explicitly provide children using the type expected
/// by the component. For example, [Provider][crate::Provider]'s children need to wrapped in
/// a [Box], while [Show][crate::Show]'s children need to be wrapped in an [Rc].
/// ## Examples
///
/// ```
/// # use leptos::{ProviderProps, ShowProps};
/// # use leptos_dom::html::p;
/// # use leptos_dom::IntoView;
/// # use leptos::prelude::*;
/// # use leptos::html::p;
/// # use leptos::IntoView;
/// # use leptos_macro::component;
/// # use std::rc::Rc;
/// #
/// #[component]
/// fn App() -> impl IntoView {
/// (
/// ProviderProps::builder()
/// .children(Box::new(|| p().child("Foo").into_view().into()))
/// // ...
/// # .value("Foo")
/// # .build(),
/// ShowProps::builder()
/// .children(Rc::new(|| p().child("Foo").into_view().into()))
/// // ...
/// # .when(|| true)
/// # .fallback(|| p().child("foo"))
/// # .build(),
/// )
/// }
/// ```
///
/// ## With ToChildren
///
/// With [ToChildren], consumers don't need to know exactly which type a component uses for
/// its children.
///
/// ```
/// # use leptos::{ProviderProps, ShowProps};
/// # use leptos_dom::html::p;
/// # use leptos_dom::IntoView;
/// # use leptos_macro::component;
/// # use std::rc::Rc;
/// # use leptos::ToChildren;
/// #
/// # use leptos::children::ToChildren;
/// use leptos::context::{Provider, ProviderProps};
/// use leptos::control_flow::{Show, ShowProps};
///
/// #[component]
/// fn App() -> impl IntoView {
/// (
/// Provider(
/// ProviderProps::builder()
/// .children(ToChildren::to_children(|| {
/// p().child("Foo").into_view().into()
/// p().child("Foo")
/// }))
/// // ...
/// # .value("Foo")
/// # .build(),
/// ShowProps::builder()
/// .value("Foo")
/// .build(),
/// ),
/// Show(
/// ShowProps::builder()
/// .children(ToChildren::to_children(|| {
/// p().child("Foo").into_view().into()
/// p().child("Foo")
/// }))
/// // ...
/// # .when(|| true)
/// # .fallback(|| p().child("foo"))
/// # .build(),
/// .when(|| true)
/// .fallback(|| p().child("foo"))
/// .build(),
/// )
/// )
/// }
pub trait ToChildren<F> {

View File

@@ -9,7 +9,7 @@ use reactive_graph::{
traits::{Get, Update, With, WithUntracked},
};
use rustc_hash::FxHashMap;
use std::{marker::PhantomData, sync::Arc};
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
use tachys::{
html::attribute::Attribute,
hydration::Cursor,
@@ -22,6 +22,29 @@ use tachys::{
};
use throw_error::{Error, ErrorHook, ErrorId};
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
/// This component lets you define a fallback that should be rendered in that
/// error case, allowing you to handle errors within a section of the interface.
///
/// ```
/// # use leptos::prelude::*;
/// #[component]
/// pub fn ErrorBoundaryExample() -> impl IntoView {
/// let (value, set_value) = signal(Ok(0));
/// let on_input =
/// move |ev| set_value.set(event_target_value(&ev).parse::<i32>());
///
/// view! {
/// <input type="text" on:input=on_input/>
/// <ErrorBoundary
/// fallback=move |_| view! { <p class="error">"Enter a valid number."</p>}
/// >
/// <p>"Value is: " {move || value.get()}</p>
/// </ErrorBoundary>
/// }
/// }
/// ```
///
/// ## Beginner's Tip: ErrorBoundary Requires Your Error To Implement std::error::Error.
/// `ErrorBoundary` requires your `Result<T,E>` to implement [IntoView](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
@@ -72,12 +95,11 @@ where
});
let hook = hook as Arc<dyn ErrorHook>;
// provide the error hook and render children
// TODO unset this outside the ErrorBoundary
throw_error::set_error_hook(Arc::clone(&hook));
let _guard = throw_error::set_error_hook(Arc::clone(&hook));
let children = children.into_inner()();
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
@@ -87,8 +109,8 @@ where
}
}
#[derive(Debug)]
struct ErrorBoundaryView<Chil, FalFn, Rndr> {
hook: Arc<dyn ErrorHook>,
boundary_id: SerializedDataId,
errors_empty: ArcMemo<bool>,
children: Chil,
@@ -145,11 +167,14 @@ where
type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
fn build(mut self) -> Self::State {
let hook = Arc::clone(&self.hook);
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
let mut children = Some(self.children.build());
RenderEffect::new(
move |prev: Option<
ErrorBoundaryViewState<Chil::State, Fal::State>,
>| {
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
if let Some(mut state) = prev {
match (self.errors_empty.get(), &mut state.fallback) {
// no errors, and was showing fallback
@@ -216,6 +241,7 @@ where
Self::Output<NewAttr>: RenderHtml<Rndr>,
{
let ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
@@ -224,6 +250,7 @@ where
rndr,
} = self;
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children: children.add_any_attr(attr.into_cloneable_owned()),
@@ -252,6 +279,7 @@ where
async fn resolve(self) -> Self::AsyncOutput {
let ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children,
@@ -260,6 +288,7 @@ where
..
} = self;
ErrorBoundaryView {
hook,
boundary_id,
errors_empty,
children: children.resolve().await,
@@ -277,6 +306,7 @@ where
mark_branches: bool,
) {
// first, attempt to serialize the children to HTML, then check for errors
let _hook = throw_error::set_error_hook(self.hook);
let mut new_buf = String::with_capacity(Chil::MIN_LENGTH);
let mut new_pos = *position;
self.children.to_html_with_buf(
@@ -309,6 +339,7 @@ where
) where
Self: Sized,
{
let _hook = throw_error::set_error_hook(self.hook);
// first, attempt to serialize the children to HTML, then check for errors
let mut new_buf = StreamBuilder::new(buf.clone_id());
let mut new_pos = *position;
@@ -319,10 +350,6 @@ where
mark_branches,
);
if let Some(sc) = Owner::current_shared_context() {
sc.seal_errors(&self.boundary_id);
}
// any thrown errors would've been caught here
if self.errors.with_untracked(|map| map.is_empty()) {
buf.append(new_buf);
@@ -345,12 +372,14 @@ where
position: &PositionState,
) -> Self::State {
let mut children = Some(self.children);
let hook = Arc::clone(&self.hook);
let cursor = cursor.to_owned();
let position = position.to_owned();
RenderEffect::new(
move |prev: Option<
ErrorBoundaryViewState<Chil::State, Fal::State>,
>| {
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
if let Some(mut state) = prev {
match (self.errors_empty.get(), &mut state.fallback) {
// no errors, and was showing fallback
@@ -424,7 +453,10 @@ impl ErrorBoundaryErrorHook {
impl ErrorHook for ErrorBoundaryErrorHook {
fn throw(&self, error: Error) -> ErrorId {
// generate a unique ID
let key = ErrorId::default(); // TODO unique ID...
let key: ErrorId = Owner::current_shared_context()
.map(|sc| sc.next_id())
.unwrap_or_default()
.into();
// register it with the shared context, so that it can be serialized from server to client
// as needed

View File

@@ -10,7 +10,7 @@ use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
/// as it avoids re-creating DOM nodes that are not being changed.
///
/// ```
/// # use leptos::*;
/// # use leptos::prelude::*;
///
/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// struct Counter {
@@ -80,25 +80,24 @@ where
mod tests {
use crate::prelude::*;
use leptos_macro::view;
use tachys::{
html::element::HtmlElement, prelude::ElementChild,
renderer::mock_dom::MockDom, view::Render,
};
use tachys::{html::element::HtmlElement, prelude::ElementChild};
#[test]
fn creates_list() {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: HtmlElement<_, _, _, MockDom> = view! {
<ol>
<For each=move || values.get() key=|i| *i let:i>
<li>{i}</li>
</For>
</ol>
};
let list = list.build();
assert_eq!(
list.el.to_debug_html(),
"<ol><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ol>"
);
Owner::new().with(|| {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: HtmlElement<_, _, _, Dom> = view! {
<ol>
<For each=move || values.get() key=|i| *i let:i>
<li>{i}</li>
</For>
</ol>
};
assert_eq!(
list.to_html(),
"<ol><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><!></\
ol>"
);
});
}
}

View File

@@ -34,8 +34,8 @@ use web_sys::{
/// should make use of indexing notation of `serde_qs`.
///
/// ```rust
/// # use leptos::*;
/// # use leptos_router::*;
/// # use leptos::prelude::*;
/// use leptos::form::ActionForm;
///
/// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
/// struct HeftyData {
@@ -45,7 +45,7 @@ use web_sys::{
///
/// #[component]
/// fn ComplexInput() -> impl IntoView {
/// let submit = Action::<VeryImportantFn, _>::server();
/// let submit = ServerAction::<VeryImportantFn>::new();
///
/// view! {
/// <ActionForm action=submit>
@@ -145,10 +145,6 @@ where
} else {
Either::Right(action_form)
}
// TODO add other attributes
/*for (attr_name, attr_value) in attributes {
action_form = action_form.attr(attr_name, attr_value);
}*/
}
/// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML
@@ -216,10 +212,6 @@ where
} else {
Either::Right(action_form)
}
// TODO add other attributes
/*for (attr_name, attr_value) in attributes {
action_form = action_form.attr(attr_name, attr_value);
}*/
}
/// Resolves a redirect location to an (absolute) URL.

View File

@@ -25,11 +25,7 @@ pub fn AutoReload(
};
let script = include_str!("reload_script.js");
view! {
<script nonce=nonce>
{format!("{script}({reload_port:?}, {protocol})")}
</script>
}
view! { <script nonce=nonce>{format!("{script}({reload_port:?}, {protocol})")}</script> }
})
}
@@ -49,6 +45,9 @@ pub fn HydrationScripts(
#[cfg(not(feature = "nonce"))]
let nonce = None::<String>;
let script = if islands {
if let Some(sc) = Owner::current_shared_context() {
sc.set_is_hydrating(false);
}
include_str!("./island_script.js")
} else {
include_str!("./hydration_script.js")
@@ -56,7 +55,13 @@ pub fn HydrationScripts(
view! {
<link rel="modulepreload" href=format!("/{pkg_path}/{output_name}.js") nonce=nonce.clone()/>
<link rel="preload" href=format!("/{pkg_path}/{wasm_output_name}.wasm") r#as="fetch" r#type="application/wasm" crossorigin=nonce.clone().unwrap_or_default()/>
<link
rel="preload"
href=format!("/{pkg_path}/{wasm_output_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({pkg_path:?}, {output_name:?}, {wasm_output_name:?})")}
</script>

View File

@@ -1,4 +1,4 @@
//#!rdeny(missing_docs)] // TODO restore
#!rdeny(missing_docs)]
#![forbid(unsafe_code)]
//! # About Leptos
//!
@@ -98,25 +98,25 @@
//! # A Simple Counter
//!
//! ```rust
//! use leptos::*;
//! use leptos::prelude::*;
//!
//! #[component]
//! pub fn SimpleCounter( initial_value: i32) -> impl IntoView {
//! pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
//! // create a reactive signal with the initial value
//! let (value, set_value) = create_signal( initial_value);
//! let (value, set_value) = signal( initial_value);
//!
//! // create event handlers for our buttons
//! // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
//! let clear = move |_| set_value.set(0);
//! let decrement = move |_| set_value.update(|value| *value -= 1);
//! let increment = move |_| set_value.update(|value| *value += 1);
//! let decrement = move |_| *set_value.write() -= 1;
//! let increment = move |_| *set_value.write() += 1;
//!
//! view! {
//!
//! <div>
//! <button on:click=clear>"Clear"</button>
//! <button on:click=decrement>"-1"</button>
//! <span>"Value: " {move || value.get().to_string()} "!"</span>
//! <span>"Value: " {value} "!"</span>
//! <button on:click=increment>"+1"</button>
//! </div>
//! }
@@ -125,18 +125,19 @@
//!
//! Leptos is easy to use with [Trunk](https://trunkrs.dev/) (or with a simple wasm-bindgen setup):
//! ```
//! # use leptos::*;
//! # if false { // can't run in doctests
//! use leptos::{mount::mount_to_body, prelude::*};
//!
//! #[component]
//! fn SimpleCounter(initial_value: i32) -> impl IntoView {
//! todo!()
//! // ...
//! # _ = initial_value;
//! }
//!
//! pub fn main() {
//! # if false { // can't run in doctest
//! mount_to_body(|| view! { <SimpleCounter initial_value=3 /> })
//! }
//! # }
//! }
//! ```
#![cfg_attr(feature = "nightly", feature(fn_traits))]
@@ -296,10 +297,17 @@ pub mod spawn {
}
}
// these reexports are used in islands
#[cfg(feature = "experimental-islands")]
#[doc(hidden)]
pub use wasm_bindgen; // used in islands
pub use serde;
#[cfg(feature = "experimental-islands")]
#[doc(hidden)]
pub use web_sys; // used in islands
pub use serde_json;
#[doc(hidden)]
pub use wasm_bindgen;
#[doc(hidden)]
pub use web_sys;
/*mod additional_attributes;
pub use additional_attributes::*;

View File

@@ -1,6 +1,8 @@
use crate::{logging, IntoView};
use any_spawner::Executor;
use reactive_graph::owner::Owner;
#[cfg(debug_assertions)]
use std::cell::Cell;
use std::marker::PhantomData;
use tachys::{
dom::body,
@@ -27,6 +29,11 @@ where
owner.forget();
}
#[cfg(debug_assertions)]
thread_local! {
static FIRST_CALL: Cell<bool> = const { Cell::new(true) };
}
#[cfg(feature = "hydrate")]
/// Runs the provided closure and mounts the result to the provided element.
pub fn hydrate_from<F, N>(
@@ -45,15 +52,19 @@ where
// already initialized, which is not an issue
_ = Executor::init_wasm_bindgen();
if !cfg!(feature = "hydrate") {
logging::warn!(
"It seems like you're trying to use Leptos in hydration mode, but \
the `hydrate` feature is not enabled on the `leptos` crate. Add \
`features = [\"hydrate\"]` to your Cargo.toml for the crate to \
work properly.\n\nNote that hydration and client-side rendering \
now use separate functions from leptos::mount: you are calling a \
hydration function."
);
#[cfg(debug_assertions)]
{
if !cfg!(feature = "hydrate") && FIRST_CALL.get() {
logging::warn!(
"It seems like you're trying to use Leptos in hydration mode, \
but the `hydrate` feature is not enabled on the `leptos` \
crate. Add `features = [\"hydrate\"]` to your Cargo.toml for \
the crate to work properly.\n\nNote that hydration and \
client-side rendering now use separate functions from \
leptos::mount: you are calling a hydration function."
);
}
FIRST_CALL.set(false);
}
// create a new reactive owner and use it as the root node to run the app
@@ -100,16 +111,20 @@ where
// already initialized, which is not an issue
_ = Executor::init_wasm_bindgen();
if !cfg!(feature = "csr") {
logging::warn!(
"It seems like you're trying to use Leptos in client-side \
rendering mode, but the `csr` feature is not enabled on the \
`leptos` crate. Add `features = [\"csr\"]` to your Cargo.toml \
for the crate to work properly.\n\nNote that hydration and \
client-side rendering now use different functions from \
leptos::mount. You are using a client-side rendering mount \
function."
);
#[cfg(debug_assertions)]
{
if !cfg!(feature = "csr") && FIRST_CALL.get() {
logging::warn!(
"It seems like you're trying to use Leptos in client-side \
rendering mode, but the `csr` feature is not enabled on the \
`leptos` crate. Add `features = [\"csr\"]` to your \
Cargo.toml for the crate to work properly.\n\nNote that \
hydration and client-side rendering now use different \
functions from leptos::mount. You are using a client-side \
rendering mount function."
);
}
FIRST_CALL.set(false);
}
// create a new reactive owner and use it as the root node to run the app
@@ -166,7 +181,7 @@ where
/// Hydrates any islands that are currently present on the page.
#[cfg(feature = "hydrate")]
pub fn hydrate_islands() {
use hydration_context::HydrateSharedContext;
use hydration_context::{HydrateSharedContext, SharedContext};
use std::sync::Arc;
// use wasm-bindgen-futures to drive the reactive system
@@ -174,8 +189,13 @@ pub fn hydrate_islands() {
// already initialized, which is not an issue
_ = Executor::init_wasm_bindgen();
#[cfg(debug_assertions)]
FIRST_CALL.set(false);
// create a new reactive owner and use it as the root node to run the app
let owner = Owner::new_root(Some(Arc::new(HydrateSharedContext::new())));
let sc = HydrateSharedContext::new();
sc.set_is_hydrating(false); // islands mode starts in "not hydrating"
let owner = Owner::new_root(Some(Arc::new(sc)));
owner.set();
std::mem::forget(owner);
}

View File

@@ -1,7 +1,7 @@
use crate::{children::TypedChildrenFn, mount, IntoView};
use leptos_dom::helpers::document;
use leptos_macro::component;
use reactive_graph::{effect::Effect, owner::Owner};
use reactive_graph::{effect::Effect, owner::Owner, untrack};
use std::sync::Arc;
/// Renders components somewhere else in the DOM.
@@ -62,12 +62,11 @@ where
container.clone()
};
// SendWrapper: this is only created in a single-threaded browser environment
let _ = mount.append_child(&container);
let handle = SendWrapper::new((
mount::mount_to(render_root.unchecked_into(), {
let children = Arc::clone(&children);
move || children()
move || untrack(|| children())
}),
mount.clone(),
container,

View File

@@ -9,7 +9,8 @@ use tachys::reactive_graph::OwnedView;
/// This prevents issues related to “context shadowing.”
///
/// ```rust
/// # use leptos::prelude::*;
/// use leptos::{context::Provider, prelude::*};
///
/// #[component]
/// pub fn App() -> impl IntoView {
/// // each Provider will only provide the value to its children

View File

@@ -31,6 +31,61 @@ use tachys::{
};
use throw_error::ErrorHookFuture;
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
/// it will render the `children`.
///
/// Each time one of the resources is loading again, it will fall back. To keep the current
/// children instead, use [Transition](crate::Transition).
///
/// Note that the `children` will be rendered initially (in order to capture the fact that
/// those resources are read under the suspense), so you cannot assume that resources read
/// synchronously have
/// `Some` value in `children`. However, you can read resources asynchronously by using
/// [Suspend](crate::prelude::Suspend).
///
/// ```
/// # use leptos::prelude::*;
/// # if false { // don't run in doctests
/// async fn fetch_cats(how_many: u32) -> Vec<String> { vec![] }
///
/// let (cat_count, set_cat_count) = signal::<u32>(1);
///
/// let cats = Resource::new(move || cat_count.get(), |count| fetch_cats(count));
///
/// view! {
/// <div>
/// <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
/// // you can access a resource synchronously
/// {move || {
/// cats.get().map(|data| {
/// data
/// .into_iter()
/// .map(|src| {
/// view! {
/// <img src={src}/>
/// }
/// })
/// .collect_view()
/// })
/// }
/// }
/// // or you can use `Suspend` to read resources asynchronously
/// {move || Suspend::new(async move {
/// cats.await
/// .into_iter()
/// .map(|src| {
/// view! {
/// <img src={src}/>
/// }
/// })
/// .collect_view()
/// })}
/// </Suspense>
/// </div>
/// }
/// # ;}
/// ```
#[component]
pub fn Suspense<Chil>(
#[prop(optional, into)] fallback: ViewFnOnce,

View File

@@ -15,7 +15,61 @@ use reactive_graph::{
use slotmap::{DefaultKey, SlotMap};
use tachys::reactive_graph::OwnedView;
/// TODO docs!
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
/// it will render the `children`.
///
/// Unlike [`Suspense`](crate::Suspense), this will not fall
/// back to the `fallback` state if there are further changes after the initial load.
///
/// Note that the `children` will be rendered initially (in order to capture the fact that
/// those resources are read under the suspense), so you cannot assume that resources read
/// synchronously have
/// `Some` value in `children`. However, you can read resources asynchronously by using
/// [Suspend](crate::prelude::Suspend).
///
/// ```
/// # use leptos::prelude::*;
/// # if false { // don't run in doctests
/// async fn fetch_cats(how_many: u32) -> Vec<String> { vec![] }
///
/// let (cat_count, set_cat_count) = signal::<u32>(1);
///
/// let cats = Resource::new(move || cat_count.get(), |count| fetch_cats(count));
///
/// view! {
/// <div>
/// <Transition fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
/// // you can access a resource synchronously
/// {move || {
/// cats.get().map(|data| {
/// data
/// .into_iter()
/// .map(|src| {
/// view! {
/// <img src={src}/>
/// }
/// })
/// .collect_view()
/// })
/// }
/// }
/// // or you can use `Suspend` to read resources asynchronously
/// {move || Suspend::new(async move {
/// cats.await
/// .into_iter()
/// .map(|src| {
/// view! {
/// <img src={src}/>
/// }
/// })
/// .collect_view()
/// })}
/// </Transition>
/// </div>
/// }
/// # ;}
/// ```
#[component]
pub fn Transition<Chil>(
/// Will be displayed while resources are pending. By default this is the empty view.

View File

@@ -1,11 +1,11 @@
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
use leptos::html::HtmlElement;
#[test]
fn simple_ssr_test() {
use leptos::prelude::*;
let runtime = create_runtime();
let (value, set_value) = signal(0);
let rendered = view! {
let rendered: HtmlElement<_, _, _, Dom> = view! {
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value.get().to_string()} "!"</span>
@@ -13,26 +13,13 @@ fn simple_ssr_test() {
</div>
};
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
assert_eq!(
rendered.into_view().to_html(),
"<div><button>-1</button><span>Value: \
0!</span><button>+1</button></div>"
);
} else {
assert!(rendered.into_view().to_html().contains(
"<div data-hk=\"0-0-0-1\"><button \
data-hk=\"0-0-0-2\">-1</button><span data-hk=\"0-0-0-3\">Value: \
<!--hk=0-0-0-4o|leptos-dyn-child-start-->0<!\
--hk=0-0-0-4c|leptos-dyn-child-end-->!</span><button \
data-hk=\"0-0-0-5\">+1</button></div>"
));
}
runtime.dispose();
assert_eq!(
rendered.to_html(),
"<div><button>-1</button><span>Value: \
<!>0<!>!</span><button>+1</button></div>"
);
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_test_with_components() {
use leptos::prelude::*;
@@ -49,35 +36,21 @@ fn ssr_test_with_components() {
}
}
let runtime = create_runtime();
let rendered = view! {
let rendered: HtmlElement<_, _, _, Dom> = view! {
<div class="counters">
<Counter initial_value=1/>
<Counter initial_value=2/>
</div>
};
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
assert_eq!(
rendered.into_view().to_html(),
"<div class=\"counters\"><div><button>-1</button><span>Value: \
1!</span><button>+1</button></div><div><button>-1</\
button><span>Value: 2!</span><button>+1</button></div></div>"
);
} else {
assert!(rendered.into_view().to_html().contains(
"<div data-hk=\"0-0-0-3\"><button \
data-hk=\"0-0-0-4\">-1</button><span data-hk=\"0-0-0-5\">Value: \
<!--hk=0-0-0-6o|leptos-dyn-child-start-->1<!\
--hk=0-0-0-6c|leptos-dyn-child-end-->!</span><button \
data-hk=\"0-0-0-7\">+1</button></div>"
));
}
runtime.dispose();
assert_eq!(
rendered.to_html(),
"<div class=\"counters\"><div><button>-1</button><span>Value: \
<!>1<!>!</span><button>+1</button></div><div><button>-1</\
button><span>Value: <!>2<!>!</span><button>+1</button></div></div>"
);
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_test_with_snake_case_components() {
use leptos::prelude::*;
@@ -86,7 +59,6 @@ fn ssr_test_with_snake_case_components() {
fn snake_case_counter(initial_value: i32) -> impl IntoView {
let (value, set_value) = signal(initial_value);
view! {
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value.get().to_string()} "!"</span>
@@ -94,111 +66,65 @@ fn ssr_test_with_snake_case_components() {
</div>
}
}
let runtime = create_runtime();
let rendered = view! {
let rendered: HtmlElement<_, _, _, Dom> = view! {
<div class="counters">
<SnakeCaseCounter initial_value=1/>
<SnakeCaseCounter initial_value=2/>
</div>
};
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
assert_eq!(
rendered.into_view().to_html(),
"<div class=\"counters\"><div><button>-1</button><span>Value: \
1!</span><button>+1</button></div><div><button>-1</\
button><span>Value: 2!</span><button>+1</button></div></div>"
);
} else {
assert!(rendered.into_view().to_html().contains(
"<div data-hk=\"0-0-0-3\"><button \
data-hk=\"0-0-0-4\">-1</button><span data-hk=\"0-0-0-5\">Value: \
<!--hk=0-0-0-6o|leptos-dyn-child-start-->1<!\
--hk=0-0-0-6c|leptos-dyn-child-end-->!</span><button \
data-hk=\"0-0-0-7\">+1</button></div>"
));
}
runtime.dispose();
assert_eq!(
rendered.to_html(),
"<div class=\"counters\"><div><button>-1</button><span>Value: \
<!>1<!>!</span><button>+1</button></div><div><button>-1</\
button><span>Value: <!>2<!>!</span><button>+1</button></div></div>"
);
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn test_classes() {
use leptos::prelude::*;
let runtime = create_runtime();
let (value, _set_value) = signal(5);
let rendered = view! {
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
let rendered: HtmlElement<_, _, _, Dom> = view! {
<div
class="my big"
class:a=move || { value.get() > 10 }
class:red=true
class:car=move || { value.get() > 1 }
></div>
};
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
assert_eq!(
rendered.into_view().to_html(),
"<div class=\"my big red car\"></div>"
);
} else {
assert!(rendered.into_view().to_html().contains(
"<div data-hk=\"0-0-0-1\" class=\"my big red car\"></div>"
));
}
runtime.dispose();
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_with_styles() {
use leptos::prelude::*;
let runtime = create_runtime();
let (_, set_value) = signal(0);
let styles = "myclass";
let rendered = view! {
class = styles,
let rendered: HtmlElement<_, _, _, Dom> = view! { class=styles,
<div>
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>
"-1"
</button>
</div>
};
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
assert_eq!(
rendered.into_view().to_html(),
"<div class=\" myclass\"><button class=\"btn \
myclass\">-1</button></div>"
);
} else {
assert!(rendered.into_view().to_html().contains(
"<div data-hk=\"0-0-0-1\" class=\" myclass\"><button \
data-hk=\"0-0-0-2\" class=\"btn myclass\">-1</button></div>"
));
}
runtime.dispose();
assert_eq!(
rendered.to_html(),
"<div class=\"myclass\"><button class=\"btn \
myclass\">-1</button></div>"
);
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_option() {
use leptos::prelude::*;
let runtime = create_runtime();
let (_, _) = signal(0);
let rendered = view! {
let rendered: HtmlElement<_, _, _, Dom> = view! { <option></option> };
<option/>
};
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
assert_eq!(rendered.into_view().to_html(), "<option></option>");
} else {
assert!(rendered
.into_view()
.to_html()
.contains("<option data-hk=\"0-0-0-1\"></option>"));
}
runtime.dispose();
assert_eq!(rendered.to_html(), "<option></option>");
}

View File

@@ -11,12 +11,8 @@ rust-version.workspace = true
[dependencies]
tachys = { workspace = true }
reactive_graph = { workspace = true }
hydration_context = { workspace = true }
or_poisoned = { workspace = true }
base64 = { version = "0.21", optional = true }
getrandom = { version = "0.2", optional = true }
js-sys = "0.3"
rand = { version = "0.8", optional = true }
send_wrapper = "0.6"
tracing = "0.1"
wasm-bindgen = "0.2"
@@ -30,11 +26,6 @@ features = ["Location"]
[features]
default = []
nightly = ["reactive_graph/nightly"]
# TODO implement nonces
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
experimental-islands = []
trace-component-props = ["tracing"]
tracing = []
[package.metadata.docs.rs]

View File

@@ -1,287 +0,0 @@
mod dyn_child;
mod each;
mod errors;
mod fragment;
mod unit;
use crate::{
hydration::{HydrationCtx, HydrationKey},
Comment, IntoView, View,
};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
pub use dyn_child::*;
pub use each::*;
pub use errors::*;
pub use fragment::*;
use leptos_reactive::{untrack_with_diagnostics, Oco};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use once_cell::unsync::OnceCell;
use std::fmt;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::rc::Rc;
pub use unit::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::JsCast;
/// The core foundational leptos components.
#[derive(Clone, PartialEq, Eq)]
pub enum CoreComponent {
/// The [Unit] component.
Unit(UnitRepr),
/// The [DynChild] component.
DynChild(DynChildRepr),
/// The [Each] component.
Each(EachRepr),
}
impl Default for CoreComponent {
fn default() -> Self {
Self::Unit(UnitRepr::default())
}
}
impl fmt::Debug for CoreComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unit(u) => u.fmt(f),
Self::DynChild(dc) => dc.fmt(f),
Self::Each(e) => e.fmt(f),
}
}
}
/// Custom leptos component.
#[derive(Clone, PartialEq, Eq)]
pub struct ComponentRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) document_fragment: web_sys::DocumentFragment,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mounted: Rc<OnceCell<()>>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) name: Oco<'static, str>,
#[cfg(debug_assertions)]
_opening: Comment,
/// The children of the component.
pub children: Vec<View>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: Option<HydrationKey>,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>,
}
impl fmt::Debug for ComponentRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;
if self.children.is_empty() {
#[cfg(debug_assertions)]
return write!(f, "<{} />", self.name);
#[cfg(not(debug_assertions))]
return f.write_str("<Component />");
} else {
#[cfg(debug_assertions)]
writeln!(f, "<{}>", self.name)?;
#[cfg(not(debug_assertions))]
f.write_str("<Component>")?;
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
for child in &self.children {
writeln!(pad_adapter, "{child:#?}")?;
}
#[cfg(debug_assertions)]
write!(f, "</{}>", self.name)?;
#[cfg(not(debug_assertions))]
f.write_str("</Component>")?;
Ok(())
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl Mountable for ComponentRepr {
fn get_mountable_node(&self) -> web_sys::Node {
if self.mounted.get().is_none() {
self.mounted.set(()).unwrap();
self.document_fragment
.unchecked_ref::<web_sys::Node>()
.to_owned()
}
// We need to prepare all children to move
else {
let opening = self.get_opening_node();
prepare_to_move(
&self.document_fragment,
&opening,
&self.closing.node,
);
self.document_fragment.clone().unchecked_into()
}
}
fn get_opening_node(&self) -> web_sys::Node {
#[cfg(debug_assertions)]
return self._opening.node.clone();
#[cfg(not(debug_assertions))]
return if let Some(child) = self.children.get(0) {
child.get_opening_node()
} else {
self.closing.node.clone()
};
}
#[inline]
fn get_closing_node(&self) -> web_sys::Node {
self.closing.node.clone()
}
}
impl From<ComponentRepr> for View {
fn from(value: ComponentRepr) -> Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
if !HydrationCtx::is_hydrating() {
for child in &value.children {
mount_child(MountKind::Before(&value.closing.node), child);
}
}
View::Component(value)
}
}
impl IntoView for ComponentRepr {
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
fn into_view(self) -> View {
self.into()
}
}
impl ComponentRepr {
/// Creates a new [`Component`].
#[inline(always)]
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
Self::new_with_id_concrete(name.into(), HydrationCtx::id())
}
/// Creates a new [`Component`] with the given hydration ID.
#[inline(always)]
pub fn new_with_id(
name: impl Into<Oco<'static, str>>,
id: Option<HydrationKey>,
) -> Self {
Self::new_with_id_concrete(name.into(), id)
}
fn new_with_id_concrete(
name: Oco<'static, str>,
id: Option<HydrationKey>,
) -> Self {
let markers = (
Comment::new(format!("</{name}>"), &id, true),
#[cfg(debug_assertions)]
Comment::new(format!("<{name}>"), &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let document_fragment = {
let fragment = crate::document().create_document_fragment();
// Insert the comments into the document fragment
// so they can serve as our references when inserting
// future nodes
if !HydrationCtx::is_hydrating() {
#[cfg(debug_assertions)]
fragment
.append_with_node_2(&markers.1.node, &markers.0.node)
.expect("append to not err");
#[cfg(not(debug_assertions))]
fragment
.append_with_node_1(&markers.0.node)
.expect("append to not err");
}
fragment
};
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mounted: Default::default(),
#[cfg(debug_assertions)]
_opening: markers.1,
closing: markers.0,
#[cfg(any(debug_assertions, feature = "ssr"))]
name,
children: Vec::with_capacity(1),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
#[cfg(debug_assertions)]
view_marker: None,
}
}
#[cfg(any(debug_assertions, feature = "ssr"))]
/// Returns the name of the component.
pub fn name(&self) -> &str {
&self.name
}
}
/// A user-defined `leptos` component.
pub struct Component<F, V>
where
F: FnOnce() -> V,
V: IntoView,
{
id: Option<HydrationKey>,
name: Oco<'static, str>,
children_fn: F,
}
impl<F, V> Component<F, V>
where
F: FnOnce() -> V,
V: IntoView,
{
/// Creates a new component.
pub fn new(name: impl Into<Oco<'static, str>>, f: F) -> Self {
Self {
id: HydrationCtx::id(),
name: name.into(),
children_fn: f,
}
}
}
impl<F, V> IntoView for Component<F, V>
where
F: FnOnce() -> V,
V: IntoView,
{
#[track_caller]
fn into_view(self) -> View {
let Self {
id,
name,
children_fn,
} = self;
let mut repr = ComponentRepr::new_with_id(name, id);
// disposed automatically when the parent scope is disposed
let child = untrack_with_diagnostics(|| children_fn().into_view());
repr.children.push(child);
repr.into_view()
}
}

View File

@@ -1,452 +0,0 @@
use crate::{
hydration::{HydrationCtx, HydrationKey},
Comment, IntoView, View,
};
use cfg_if::cfg_if;
use std::{cell::RefCell, fmt, ops::Deref, rc::Rc};
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable, Text};
use leptos_reactive::create_render_effect;
use wasm_bindgen::JsCast;
}
}
/// The internal representation of the [`DynChild`] core-component.
#[derive(Clone, PartialEq, Eq)]
pub struct DynChildRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment: web_sys::DocumentFragment,
#[cfg(debug_assertions)]
opening: Comment,
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: Option<HydrationKey>,
}
impl fmt::Debug for DynChildRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;
f.write_str("<DynChild>\n")?;
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
writeln!(
pad_adapter,
"{:#?}",
self.child.borrow().deref().deref().as_ref().unwrap()
)?;
f.write_str("</DynChild>")
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl Mountable for DynChildRepr {
fn get_mountable_node(&self) -> web_sys::Node {
if self.document_fragment.child_nodes().length() != 0 {
self.document_fragment.clone().unchecked_into()
} else {
let opening = self.get_opening_node();
prepare_to_move(
&self.document_fragment,
&opening,
&self.closing.node,
);
self.document_fragment.clone().unchecked_into()
}
}
fn get_opening_node(&self) -> web_sys::Node {
#[cfg(debug_assertions)]
return self.opening.node.clone();
#[cfg(not(debug_assertions))]
return self
.child
.borrow()
.as_ref()
.as_ref()
.unwrap()
.get_opening_node();
}
fn get_closing_node(&self) -> web_sys::Node {
self.closing.node.clone()
}
}
impl DynChildRepr {
fn new_with_id(id: Option<HydrationKey>) -> Self {
let markers = (
Comment::new("</DynChild>", &id, true),
#[cfg(debug_assertions)]
Comment::new("<DynChild>", &id, false),
);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let document_fragment = {
let fragment = crate::document().create_document_fragment();
// Insert the comments into the document fragment
// so they can serve as our references when inserting
// future nodes
if !HydrationCtx::is_hydrating() {
#[cfg(debug_assertions)]
fragment
.append_with_node_2(&markers.1.node, &markers.0.node)
.unwrap();
#[cfg(not(debug_assertions))]
fragment.append_with_node_1(&markers.0.node).unwrap();
}
fragment
};
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
document_fragment,
#[cfg(debug_assertions)]
opening: markers.1,
child: Default::default(),
closing: markers.0,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
}
}
/// Represents any [`View`] that can change over time.
pub struct DynChild<CF, N>
where
CF: Fn() -> N + 'static,
N: IntoView,
{
id: Option<HydrationKey>,
child_fn: CF,
}
impl<CF, N> DynChild<CF, N>
where
CF: Fn() -> N + 'static,
N: IntoView,
{
/// Creates a new dynamic child which will re-render whenever it's
/// signal dependencies change.
#[track_caller]
#[inline(always)]
pub fn new(child_fn: CF) -> Self {
Self::new_with_id(HydrationCtx::id(), child_fn)
}
#[doc(hidden)]
#[track_caller]
#[inline(always)]
pub const fn new_with_id(id: Option<HydrationKey>, child_fn: CF) -> Self {
Self { id, child_fn }
}
}
impl<CF, N> IntoView for DynChild<CF, N>
where
CF: Fn() -> N + 'static,
N: IntoView,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "<DynChild />", skip_all)
)]
#[inline]
fn into_view(self) -> View {
// concrete inner function
#[inline(never)]
fn create_dyn_view(
component: DynChildRepr,
child_fn: Box<dyn Fn() -> View>,
) -> DynChildRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let closing = component.closing.node.clone();
let child = component.child.clone();
#[cfg(all(
debug_assertions,
target_arch = "wasm32",
feature = "web"
))]
let span = tracing::Span::current();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_render_effect(
move |prev_run: Option<Option<web_sys::Node>>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
let new_child = child_fn().into_view();
let mut child_borrow = child.borrow_mut();
// Is this at least the second time we are loading a child?
if let Some(prev_t) = prev_run {
let child = child_borrow.take().unwrap();
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& 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
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
// nb: the match/ownership gymnastics here
// are so that, if we can reuse the text node,
// we can take ownership of new_t so we don't clone
// the contents, which in O(n) on the length of the text
if matches!(new_child, View::Text(_)) {
if !was_child_moved && child != new_child {
let mut new_t = match new_child {
View::Text(t) => t,
_ => unreachable!(),
};
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
// replace new_t's text node with the prev node
// see discussion: https://github.com/leptos-rs/leptos/pull/1472
new_t.node = prev_t.clone();
let new_child = View::Text(new_t);
**child_borrow = Some(new_child);
Some(prev_t)
} else {
let new_t = new_child.as_text().unwrap();
mount_child(
MountKind::Before(&closing),
&new_child,
);
**child_borrow = Some(new_child.clone());
Some(new_t.node.clone())
}
}
// Child is not a text node, so we can remove the previous
// text node
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
// Mount the new child, and we're done
mount_child(
MountKind::Before(&closing),
&new_child,
);
**child_borrow = Some(new_child);
None
}
}
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
if !HydrationCtx::is_hydrating() {
let same_child = child == new_child;
if !was_child_moved && !same_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
match child {
View::CoreComponent(
crate::CoreComponent::DynChild(
child,
),
) => {
let start =
child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
View::Component(child) => {
let start =
child.get_opening_node();
let end = child.closing.node;
prepare_to_move(
&child.document_fragment,
&start,
&end,
);
}
_ => unmount_child(&start, end),
}
}
// Mount the new child
// If it's the same child, don't re-mount
if !same_child {
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
}
// We want to reuse text nodes, so hold onto it if
// our child is one
let t =
new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
t
};
ret
}
// Otherwise, we know for sure this is our first time
else {
// If it's a text node, we want to use the old text node
// as the text node for the DynChild, rather than the new
// text node being created during hydration
let new_child = if HydrationCtx::is_hydrating()
&& new_child.get_text().is_some()
{
let t = closing
.previous_non_view_marker_sibling()
.unwrap()
.unchecked_into::<web_sys::Text>();
let new_child = match new_child {
View::Text(text) => text,
_ => unreachable!(),
};
t.set_data(&new_child.content);
View::Text(Text {
node: t.unchecked_into(),
content: new_child.content,
})
} else {
new_child
};
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(
MountKind::Before(&closing),
&new_child,
);
}
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
t
}
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view();
**child.borrow_mut() = Some(new_child);
}
component
}
// monomorphized outer function
let Self { id, child_fn } = self;
let component = DynChildRepr::new_with_id(id);
let component = create_dyn_view(
component,
Box::new(move || child_fn().into_view()),
);
View::CoreComponent(crate::CoreComponent::DynChild(component))
}
}
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use web_sys::Node;
pub(crate) 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 {
#[cfg_attr(not(debug_assertions), inline(always))]
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()
}
}
}
#[cfg_attr(not(debug_assertions), inline(always))]
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()
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,166 +0,0 @@
use crate::{HydrationCtx, IntoView};
use cfg_if::cfg_if;
use leptos_reactive::{signal_prelude::*, use_context};
use server_fn::error::Error;
use std::{borrow::Cow, collections::HashMap};
/// A struct to hold all the possible errors that could be provided by child Views
#[derive(Debug, Clone, Default)]
#[repr(transparent)]
pub struct Errors(HashMap<ErrorKey, Error>);
/// A unique key for an error that occurs at a particular location in the user interface.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct ErrorKey(Cow<'static, str>);
impl<T> From<T> for ErrorKey
where
T: Into<Cow<'static, str>>,
{
#[inline(always)]
fn from(key: T) -> ErrorKey {
ErrorKey(key.into())
}
}
impl IntoIterator for Errors {
type Item = (ErrorKey, Error);
type IntoIter = IntoIter;
#[inline(always)]
fn into_iter(self) -> Self::IntoIter {
IntoIter(self.0.into_iter())
}
}
/// An owning iterator over all the errors contained in the [`Errors`] struct.
#[repr(transparent)]
pub struct IntoIter(std::collections::hash_map::IntoIter<ErrorKey, Error>);
impl Iterator for IntoIter {
type Item = (ErrorKey, Error);
#[inline(always)]
fn next(
&mut self,
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
self.0.next()
}
}
/// An iterator over all the errors contained in the [`Errors`] struct.
#[repr(transparent)]
pub struct Iter<'a>(std::collections::hash_map::Iter<'a, ErrorKey, Error>);
impl<'a> Iterator for Iter<'a> {
type Item = (&'a ErrorKey, &'a Error);
#[inline(always)]
fn next(
&mut self,
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
self.0.next()
}
}
impl<T, E> IntoView for Result<T, E>
where
T: IntoView + 'static,
E: Into<Error>,
{
fn into_view(self) -> crate::View {
let id = ErrorKey(
HydrationCtx::peek()
.map(|n| n.to_string())
.unwrap_or_default()
.into(),
);
let errors = use_context::<RwSignal<Errors>>();
match self {
Ok(stuff) => {
if let Some(errors) = errors {
errors.update(|errors| {
errors.0.remove(&id);
});
}
stuff.into_view()
}
Err(error) => {
let error = error.into();
match errors {
Some(errors) => {
errors.update({
#[cfg(all(
target_arch = "wasm32",
feature = "web"
))]
let id = id.clone();
move |errors: &mut Errors| errors.insert(id, error)
});
// remove the error from the list if this drops,
// i.e., if it's in a DynChild that switches from Err to Ok
// Only can run on the client, will panic on the server
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove(&id);
});
});
});
}
}
}
None => {
#[cfg(debug_assertions)]
warn!(
"No ErrorBoundary components found! Returning \
errors will not be handled and will silently \
disappear"
);
}
}
().into_view()
}
}
}
}
impl Errors {
/// Returns `true` if there are no errors.
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: ErrorKey, error: E)
where
E: Into<Error>,
{
self.0.insert(key, error.into());
}
/// Add an error with the default key for errors outside the reactive system
pub fn insert_with_default_key<E>(&mut self, error: E)
where
E: Into<Error>,
{
self.0.insert(Default::default(), error.into());
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove(&mut self, key: &ErrorKey) -> Option<Error> {
self.0.remove(key)
}
/// An iterator over all the errors, in arbitrary order.
#[inline(always)]
pub fn iter(&self) -> Iter<'_> {
Iter(self.0.iter())
}
}

View File

@@ -1,117 +0,0 @@
use crate::{
hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View,
};
/// Trait for converting any iterable into a [`Fragment`].
pub trait IntoFragment {
/// Consumes this type, returning [`Fragment`].
fn into_fragment(self) -> Fragment;
}
impl<I, V> IntoFragment for I
where
I: IntoIterator<Item = V>,
V: IntoView,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
fn into_fragment(self) -> Fragment {
self.into_iter().map(|v| v.into_view()).collect()
}
}
/// Represents a group of [`views`](View).
#[must_use = "You are creating a Fragment but not using it. An unused view can \
cause your view to be rendered as () unexpectedly, and it can \
also cause issues with client-side hydration."]
#[derive(Debug, Clone)]
pub struct Fragment {
id: Option<HydrationKey>,
/// The nodes contained in the fragment.
pub nodes: Vec<View>,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>,
}
impl FromIterator<View> for Fragment {
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
Fragment::new(iter.into_iter().collect())
}
}
impl From<View> for Fragment {
fn from(view: View) -> Self {
Fragment::new(vec![view])
}
}
impl From<Fragment> for View {
fn from(value: Fragment) -> Self {
let mut frag = ComponentRepr::new_with_id("", value.id);
#[cfg(debug_assertions)]
{
frag.view_marker = value.view_marker;
}
frag.children = value.nodes;
frag.into()
}
}
impl Fragment {
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
#[inline(always)]
pub fn new(nodes: Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes)
}
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
#[inline(always)]
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
Self::new_with_id(HydrationCtx::id(), nodes())
}
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
#[inline(always)]
pub const fn new_with_id(
id: Option<HydrationKey>,
nodes: Vec<View>,
) -> Self {
Self {
id,
nodes,
#[cfg(debug_assertions)]
view_marker: None,
}
}
/// Gives access to the [`View`] children contained within the fragment.
#[inline(always)]
pub fn as_children(&self) -> &[View] {
&self.nodes
}
/// Returns the fragment's hydration ID.
#[inline(always)]
pub fn id(&self) -> &Option<HydrationKey> {
&self.id
}
#[cfg(debug_assertions)]
/// Adds an optional marker indicating the view macro source.
pub fn with_view_marker(mut self, marker: impl Into<String>) -> Self {
self.view_marker = Some(marker.into());
self
}
}
impl IntoView for Fragment {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
fn into_view(self) -> View {
self.into()
}
}

View File

@@ -1,73 +0,0 @@
use cfg_if::cfg_if;
use std::fmt;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use crate::Mountable;
use wasm_bindgen::JsCast;
} else {
use crate::hydration::HydrationKey;
}
}
use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
/// The internal representation of the [`Unit`] core-component.
#[derive(Clone, PartialEq, Eq)]
pub struct UnitRepr {
comment: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: Option<HydrationKey>,
}
impl fmt::Debug for UnitRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<() />")
}
}
impl Default for UnitRepr {
fn default() -> Self {
let id = HydrationCtx::id();
Self {
comment: Comment::new("<() />", &id, true),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl Mountable for UnitRepr {
#[inline(always)]
fn get_mountable_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
#[inline(always)]
fn get_opening_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
#[inline(always)]
fn get_closing_node(&self) -> web_sys::Node {
self.comment.node.clone().unchecked_into()
}
}
/// The unit `()` leptos counterpart.
#[derive(Clone, Copy, Debug)]
pub struct Unit;
impl IntoView for Unit {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "<() />", skip_all)
)]
fn into_view(self) -> crate::View {
let component = UnitRepr::default();
View::CoreComponent(CoreComponent::Unit(component))
}
}

View File

@@ -1,83 +0,0 @@
use crate::{html::AnyElement, HtmlElement};
use std::rc::Rc;
/// Trait for a directive handler function.
/// This is used so it's possible to use functions with one or two
/// parameters as directive handlers.
///
/// You can use directives like the following.
///
/// ```
/// # use leptos::{*, html::AnyElement};
///
/// // This doesn't take an attribute value
/// fn my_directive(el: HtmlElement<AnyElement>) {
/// // do sth
/// }
///
/// // This requires an attribute value
/// fn another_directive(el: HtmlElement<AnyElement>, params: i32) {
/// // do sth
/// }
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// view! {
/// // no attribute value
/// <div use:my_directive></div>
///
/// // with an attribute value
/// <div use:another_directive=8></div>
/// }
/// }
/// ```
///
/// A directive is just syntactic sugar for
///
/// ```ignore
/// let node_ref = create_node_ref();
///
/// create_effect(move |_| {
/// if let Some(el) = node_ref.get() {
/// directive_func(el, possibly_some_param);
/// }
/// });
/// ```
///
/// A directive can be a function with one or two parameters.
/// The first is the element the directive is added to and the optional
/// second is the parameter that is provided in the attribute.
pub trait Directive<T: ?Sized, P> {
/// Calls the handler function
fn run(&self, el: HtmlElement<AnyElement>, param: P);
}
impl<F> Directive<(HtmlElement<AnyElement>,), ()> for F
where
F: Fn(HtmlElement<AnyElement>),
{
fn run(&self, el: HtmlElement<AnyElement>, _: ()) {
self(el)
}
}
impl<F, P> Directive<(HtmlElement<AnyElement>, P), P> for F
where
F: Fn(HtmlElement<AnyElement>, P),
{
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
self(el, param);
}
}
impl<T: ?Sized, P> Directive<T, P> for Rc<dyn Directive<T, P>> {
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
(**self).run(el, param)
}
}
impl<T: ?Sized, P> Directive<T, P> for Box<dyn Directive<T, P>> {
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
(**self).run(el, param);
}
}

View File

@@ -1,213 +0,0 @@
pub mod typed;
use leptos_reactive::Oco;
use std::{cell::RefCell, collections::HashSet};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::{
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
UnwrapThrowExt,
};
thread_local! {
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Oco<'static, str>>> = RefCell::new(HashSet::new());
}
// Used in template macro
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(always)]
pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
target: &web_sys::Element,
event: E,
#[allow(unused_mut)] // used for tracing in debug
mut event_handler: impl FnMut(E::EventType) + 'static,
) {
let event_name = event.name();
let event_handler = Box::new(event_handler);
if E::BUBBLES {
add_event_listener(
target,
event.event_delegation_key(),
event_name,
event_handler,
&None,
);
} else {
add_event_listener_undelegated(
target,
&event_name,
event_handler,
&None,
);
}
}
/// Adds an event listener to the target DOM element using implicit event delegation.
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn add_event_listener<E>(
target: &web_sys::Element,
key: Oco<'static, str>,
event_name: Oco<'static, str>,
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
options: &Option<web_sys::AddEventListenerOptions>,
) where
E: FromWasmAbi + 'static,
{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = Box::new(move |e| {
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit(prev);
});
}
}
let cb = Closure::wrap(cb as Box<dyn FnMut(E)>).into_js_value();
let key = intern(&key);
debug_assert_eq!(
Ok(false),
js_sys::Reflect::has(target, &JsValue::from_str(&key)),
"Error while adding {key} event listener, a listener of type {key} \
already present."
);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
add_delegated_event_listener(&key, event_name, options);
}
#[doc(hidden)]
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn add_event_listener_undelegated<E>(
target: &web_sys::Element,
event_name: &str,
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
options: &Option<web_sys::AddEventListenerOptions>,
) where
E: FromWasmAbi + 'static,
{
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = Box::new(move |e| {
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit(prev);
});
}
}
let event_name = intern(event_name);
let cb = Closure::wrap(cb as Box<dyn FnMut(E)>).into_js_value();
if let Some(options) = options {
_ = target
.add_event_listener_with_callback_and_add_event_listener_options(
event_name,
cb.unchecked_ref(),
options,
);
} else {
_ = target
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
}
}
// cf eventHandler in ryansolid/dom-expressions
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn add_delegated_event_listener(
key: &str,
event_name: Oco<'static, str>,
options: &Option<web_sys::AddEventListenerOptions>,
) {
GLOBAL_EVENTS.with(|global_events| {
let mut events = global_events.borrow_mut();
if !events.contains(&event_name) {
// create global handler
let key = JsValue::from_str(&key);
let handler = move |ev: web_sys::Event| {
let target = ev.target();
let node = ev.composed_path().get(0);
let mut node = if node.is_undefined() || node.is_null() {
JsValue::from(target)
} else {
node
};
// TODO reverse Shadow DOM retargetting
// TODO simulate currentTarget
while !node.is_null() {
let node_is_disabled = js_sys::Reflect::get(
&node,
&JsValue::from_str("disabled"),
)
.unwrap_throw()
.is_truthy();
if !node_is_disabled {
let maybe_handler =
js_sys::Reflect::get(&node, &key).unwrap_throw();
if !maybe_handler.is_undefined() {
let f = maybe_handler
.unchecked_ref::<js_sys::Function>();
if let Err(e) = f.call1(&node, &ev) {
wasm_bindgen::throw_val(e);
}
if ev.cancel_bubble() {
return;
}
}
}
// navigate up tree
if let Some(parent) =
node.unchecked_ref::<web_sys::Node>().parent_node()
{
node = parent.into()
} else if let Some(root) = node.dyn_ref::<web_sys::ShadowRoot>() {
node = root.host().unchecked_into();
} else {
node = JsValue::null()
}
}
};
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let handler = move |e| {
let _guard = span.enter();
handler(e);
};
}
}
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
let handler = Closure::wrap(handler).into_js_value();
if let Some(options) = options {
_ = crate::window().add_event_listener_with_callback_and_add_event_listener_options(
&event_name,
handler.unchecked_ref(),
options,
);
} else {
_ = crate::window().add_event_listener_with_callback(
&event_name,
handler.unchecked_ref(),
);
}
// register that we've created handler
events.insert(event_name);
}
})
}

View File

@@ -1,681 +0,0 @@
//! Types for all DOM events.
use leptos_reactive::Oco;
use std::marker::PhantomData;
use wasm_bindgen::convert::FromWasmAbi;
/// A trait for converting types into [web_sys events](web_sys).
pub trait EventDescriptor: Clone {
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
type EventType: FromWasmAbi;
/// Indicates if this event bubbles. For example, `click` bubbles,
/// but `focus` does not.
///
/// If this is true, then the event will be delegated globally,
/// otherwise, event listeners will be directly attached to the element.
const BUBBLES: bool;
/// The name of the event, such as `click` or `mouseover`.
fn name(&self) -> Oco<'static, str>;
/// The key used for event delegation.
fn event_delegation_key(&self) -> Oco<'static, str>;
/// Return the options for this type. This is only used when you create a [`Custom`] event
/// handler.
#[inline(always)]
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
&None
}
}
/// Overrides the [`EventDescriptor::BUBBLES`] value to always return
/// `false`, which forces the event to not be globally delegated.
#[derive(Clone, Debug)]
#[allow(non_camel_case_types)]
pub struct undelegated<Ev: EventDescriptor>(pub Ev);
impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
type EventType = Ev::EventType;
#[inline(always)]
fn name(&self) -> Oco<'static, str> {
self.0.name()
}
#[inline(always)]
fn event_delegation_key(&self) -> Oco<'static, str> {
self.0.event_delegation_key()
}
const BUBBLES: bool = false;
}
/// A custom event.
#[derive(Debug)]
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
name: Oco<'static, str>,
options: Option<web_sys::AddEventListenerOptions>,
_event_type: PhantomData<E>,
}
impl<E: FromWasmAbi> Clone for Custom<E> {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
options: self.options.clone(),
_event_type: PhantomData,
}
}
}
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
type EventType = E;
fn name(&self) -> Oco<'static, str> {
self.name.clone()
}
fn event_delegation_key(&self) -> Oco<'static, str> {
format!("$$${}", self.name).into()
}
const BUBBLES: bool = false;
#[inline(always)]
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
&self.options
}
}
impl<E: FromWasmAbi> Custom<E> {
/// Creates a custom event type that can be used within
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
/// which are not covered in the [`ev`](crate::ev) module.
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
Self {
name: name.into(),
options: None,
_event_type: PhantomData,
}
}
/// Modify the [`AddEventListenerOptions`] used for this event listener.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # let canvas_ref: NodeRef<html::Canvas> = create_node_ref();
/// # if false {
/// let mut non_passive_wheel = ev::Custom::<ev::WheelEvent>::new("wheel");
/// let options = non_passive_wheel.options_mut();
/// options.passive(false);
/// canvas_ref.on_load(move |canvas: HtmlElement<html::Canvas>| {
/// canvas.on(non_passive_wheel, move |_event| {
/// // Handle _event
/// });
/// });
/// # }
/// # runtime.dispose();
/// ```
///
/// [`AddEventListenerOptions`]: web_sys::AddEventListenerOptions
pub fn options_mut(&mut self) -> &mut web_sys::AddEventListenerOptions {
self.options
.get_or_insert_with(web_sys::AddEventListenerOptions::new)
}
}
/// Type that can respond to DOM events
pub trait DOMEventResponder: Sized {
/// Adds handler to specified event
fn add<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self;
/// Same as [add](DOMEventResponder::add), but with [`EventHandler`]
#[inline]
fn add_handler(self, handler: impl EventHandler) -> Self {
handler.attach(self)
}
}
impl<T> DOMEventResponder for crate::HtmlElement<T>
where
T: crate::html::ElementDescriptor + 'static,
{
#[inline(always)]
fn add<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self {
self.on(event, handler)
}
}
impl DOMEventResponder for crate::View {
#[inline(always)]
fn add<E: EventDescriptor + 'static>(
self,
event: E,
handler: impl FnMut(E::EventType) + 'static,
) -> Self {
self.on(event, handler)
}
}
/// A statically typed event handler.
pub enum EventHandlerFn {
/// `keydown` event handler.
Keydown(Box<dyn FnMut(KeyboardEvent)>),
/// `keyup` event handler.
Keyup(Box<dyn FnMut(KeyboardEvent)>),
/// `keypress` event handler.
Keypress(Box<dyn FnMut(KeyboardEvent)>),
/// `click` event handler.
Click(Box<dyn FnMut(MouseEvent)>),
/// `dblclick` event handler.
Dblclick(Box<dyn FnMut(MouseEvent)>),
/// `mousedown` event handler.
Mousedown(Box<dyn FnMut(MouseEvent)>),
/// `mouseup` event handler.
Mouseup(Box<dyn FnMut(MouseEvent)>),
/// `mouseenter` event handler.
Mouseenter(Box<dyn FnMut(MouseEvent)>),
/// `mouseleave` event handler.
Mouseleave(Box<dyn FnMut(MouseEvent)>),
/// `mouseout` event handler.
Mouseout(Box<dyn FnMut(MouseEvent)>),
/// `mouseover` event handler.
Mouseover(Box<dyn FnMut(MouseEvent)>),
/// `mousemove` event handler.
Mousemove(Box<dyn FnMut(MouseEvent)>),
/// `wheel` event handler.
Wheel(Box<dyn FnMut(WheelEvent)>),
/// `touchstart` event handler.
Touchstart(Box<dyn FnMut(TouchEvent)>),
/// `touchend` event handler.
Touchend(Box<dyn FnMut(TouchEvent)>),
/// `touchcancel` event handler.
Touchcancel(Box<dyn FnMut(TouchEvent)>),
/// `touchmove` event handler.
Touchmove(Box<dyn FnMut(TouchEvent)>),
/// `pointerenter` event handler.
Pointerenter(Box<dyn FnMut(PointerEvent)>),
/// `pointerleave` event handler.
Pointerleave(Box<dyn FnMut(PointerEvent)>),
/// `pointerdown` event handler.
Pointerdown(Box<dyn FnMut(PointerEvent)>),
/// `pointerup` event handler.
Pointerup(Box<dyn FnMut(PointerEvent)>),
/// `pointercancel` event handler.
Pointercancel(Box<dyn FnMut(PointerEvent)>),
/// `pointerout` event handler.
Pointerout(Box<dyn FnMut(PointerEvent)>),
/// `pointerover` event handler.
Pointerover(Box<dyn FnMut(PointerEvent)>),
/// `pointermove` event handler.
Pointermove(Box<dyn FnMut(PointerEvent)>),
/// `drag` event handler.
Drag(Box<dyn FnMut(DragEvent)>),
/// `dragend` event handler.
Dragend(Box<dyn FnMut(DragEvent)>),
/// `dragenter` event handler.
Dragenter(Box<dyn FnMut(DragEvent)>),
/// `dragleave` event handler.
Dragleave(Box<dyn FnMut(DragEvent)>),
/// `dragstart` event handler.
Dragstart(Box<dyn FnMut(DragEvent)>),
/// `drop` event handler.
Drop(Box<dyn FnMut(DragEvent)>),
/// `blur` event handler.
Blur(Box<dyn FnMut(FocusEvent)>),
/// `focusout` event handler.
Focusout(Box<dyn FnMut(FocusEvent)>),
/// `focus` event handler.
Focus(Box<dyn FnMut(FocusEvent)>),
/// `focusin` event handler.
Focusin(Box<dyn FnMut(FocusEvent)>),
}
/// Type that can be used to handle DOM events
pub trait EventHandler {
/// Attaches event listener to any target that can respond to DOM events
fn attach<T: DOMEventResponder>(self, target: T) -> T;
}
impl<T, const N: usize> EventHandler for [T; N]
where
T: EventHandler,
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
let mut target = target;
for item in self {
target = item.attach(target);
}
target
}
}
impl<T> EventHandler for Option<T>
where
T: EventHandler,
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
match self {
Some(event_handler) => event_handler.attach(target),
None => target,
}
}
}
macro_rules! tc {
($($ty:ident),*) => {
impl<$($ty),*> EventHandler for ($($ty,)*)
where
$($ty: EventHandler),*
{
#[inline]
fn attach<RES: DOMEventResponder>(self, target: RES) -> RES {
::paste::paste! {
let (
$(
[<$ty:lower>],)*
) = self;
$(
let target = [<$ty:lower>].attach(target);
)*
target
}
}
}
};
}
tc!(A);
tc!(A, B);
tc!(A, B, C);
tc!(A, B, C, D);
tc!(A, B, C, D, E);
tc!(A, B, C, D, E, F);
tc!(A, B, C, D, E, F, G);
tc!(A, B, C, D, E, F, G, H);
tc!(A, B, C, D, E, F, G, H, I);
tc!(A, B, C, D, E, F, G, H, I, J);
tc!(A, B, C, D, E, F, G, H, I, J, K);
tc!(A, B, C, D, E, F, G, H, I, J, K, L);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y);
#[rustfmt::skip]
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z);
macro_rules! collection_callback {
{$(
$collection:ident
),* $(,)?} => {
$(
impl<T> EventHandler for $collection<T>
where
T: EventHandler
{
#[inline]
fn attach<R: DOMEventResponder>(self, target: R) -> R {
let mut target = target;
for item in self {
target = item.attach(target);
}
target
}
}
)*
};
}
use std::collections::{BTreeSet, BinaryHeap, HashSet, LinkedList, VecDeque};
collection_callback! {
Vec,
BTreeSet,
BinaryHeap,
HashSet,
LinkedList,
VecDeque,
}
macro_rules! generate_event_types {
{$(
$( #[$does_not_bubble:ident] )?
$( $event:ident )+ : $web_event:ident
),* $(,)?} => {
::paste::paste! {
$(
#[doc = "The `" [< $($event)+ >] "` event, which receives [" $web_event "](web_sys::" $web_event ") as its argument."]
#[derive(Copy, Clone, Debug)]
#[allow(non_camel_case_types)]
pub struct [<$( $event )+ >];
impl EventDescriptor for [< $($event)+ >] {
type EventType = web_sys::$web_event;
#[inline(always)]
fn name(&self) -> Oco<'static, str> {
stringify!([< $($event)+ >]).into()
}
#[inline(always)]
fn event_delegation_key(&self) -> Oco<'static, str> {
concat!("$$$", stringify!([< $($event)+ >])).into()
}
const BUBBLES: bool = true $(&& generate_event_types!($does_not_bubble))?;
}
)*
/// An enum holding all basic event types with their respective handlers.
///
/// It currently omits [`Custom`] and [`undelegated`] variants.
#[non_exhaustive]
pub enum GenericEventHandler {
$(
#[doc = "Variant mapping [`struct@" [< $($event)+ >] "`] to its event handler type."]
[< $($event:camel)+ >]([< $($event)+ >], Box<dyn FnMut($web_event) + 'static>),
)*
}
impl ::core::fmt::Debug for GenericEventHandler {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
$(
Self::[< $($event:camel)+ >](event, _) => f
.debug_tuple(stringify!([< $($event:camel)+ >]))
.field(&event)
.field(&::std::any::type_name::<Box<dyn FnMut($web_event) + 'static>>())
.finish(),
)*
}
}
}
impl EventHandler for GenericEventHandler {
fn attach<T: DOMEventResponder>(self, target: T) -> T {
match self {
$(
Self::[< $($event:camel)+ >](event, handler) => target.add(event, handler),
)*
}
}
}
$(
impl<F> From<([< $($event)+ >], F)> for GenericEventHandler
where
F: FnMut($web_event) + 'static
{
fn from(value: ([< $($event)+ >], F)) -> Self {
Self::[< $($event:camel)+ >](value.0, Box::new(value.1))
}
}
// NOTE: this could become legal in future and would save us from useless allocations
//impl<F> From<([< $($event)+ >], Box<F>)> for GenericEventHandler
//where
// F: FnMut($web_event) + 'static
//{
// fn from(value: ([< $($event)+ >], Box<F>)) -> Self {
// Self::[< $($event:camel)+ >](value.0, value.1)
// }
//}
impl<F> EventHandler for ([< $($event)+ >], F)
where
F: FnMut($web_event) + 'static
{
fn attach<L: DOMEventResponder>(self, target: L) -> L {
target.add(self.0, self.1)
}
}
)*
}
};
(does_not_bubble) => { false }
}
generate_event_types! {
// =========================================================
// WindowEventHandlersEventMap
// =========================================================
#[does_not_bubble]
after print: Event,
#[does_not_bubble]
before print: Event,
#[does_not_bubble]
before unload: BeforeUnloadEvent,
#[does_not_bubble]
gamepad connected: GamepadEvent,
#[does_not_bubble]
gamepad disconnected: GamepadEvent,
hash change: HashChangeEvent,
#[does_not_bubble]
language change: Event,
#[does_not_bubble]
message: MessageEvent,
#[does_not_bubble]
message error: MessageEvent,
#[does_not_bubble]
offline: Event,
#[does_not_bubble]
online: Event,
#[does_not_bubble]
page hide: PageTransitionEvent,
#[does_not_bubble]
page show: PageTransitionEvent,
pop state: PopStateEvent,
rejection handled: PromiseRejectionEvent,
#[does_not_bubble]
storage: StorageEvent,
#[does_not_bubble]
unhandled rejection: PromiseRejectionEvent,
#[does_not_bubble]
unload: Event,
// =========================================================
// GlobalEventHandlersEventMap
// =========================================================
#[does_not_bubble]
abort: UiEvent,
animation cancel: AnimationEvent,
animation end: AnimationEvent,
animation iteration: AnimationEvent,
animation start: AnimationEvent,
aux click: MouseEvent,
before input: InputEvent,
#[does_not_bubble]
blur: FocusEvent,
#[does_not_bubble]
can play: Event,
#[does_not_bubble]
can play through: Event,
change: Event,
click: MouseEvent,
#[does_not_bubble]
close: Event,
composition end: CompositionEvent,
composition start: CompositionEvent,
composition update: CompositionEvent,
context menu: MouseEvent,
#[does_not_bubble]
cue change: Event,
dbl click: MouseEvent,
drag: DragEvent,
drag end: DragEvent,
drag enter: DragEvent,
drag leave: DragEvent,
drag over: DragEvent,
drag start: DragEvent,
drop: DragEvent,
#[does_not_bubble]
duration change: Event,
#[does_not_bubble]
emptied: Event,
#[does_not_bubble]
ended: Event,
#[does_not_bubble]
error: ErrorEvent,
#[does_not_bubble]
focus: FocusEvent,
#[does_not_bubble]
focus in: FocusEvent,
#[does_not_bubble]
focus out: FocusEvent,
form data: Event, // web_sys does not include `FormDataEvent`
#[does_not_bubble]
got pointer capture: PointerEvent,
input: Event,
#[does_not_bubble]
invalid: Event,
key down: KeyboardEvent,
key press: KeyboardEvent,
key up: KeyboardEvent,
#[does_not_bubble]
load: Event,
#[does_not_bubble]
loaded data: Event,
#[does_not_bubble]
loaded metadata: Event,
#[does_not_bubble]
load start: Event,
lost pointer capture: PointerEvent,
mouse down: MouseEvent,
#[does_not_bubble]
mouse enter: MouseEvent,
#[does_not_bubble]
mouse leave: MouseEvent,
mouse move: MouseEvent,
mouse out: MouseEvent,
mouse over: MouseEvent,
mouse up: MouseEvent,
#[does_not_bubble]
pause: Event,
#[does_not_bubble]
play: Event,
#[does_not_bubble]
playing: Event,
pointer cancel: PointerEvent,
pointer down: PointerEvent,
#[does_not_bubble]
pointer enter: PointerEvent,
#[does_not_bubble]
pointer leave: PointerEvent,
pointer move: PointerEvent,
pointer out: PointerEvent,
pointer over: PointerEvent,
pointer up: PointerEvent,
#[does_not_bubble]
progress: ProgressEvent,
#[does_not_bubble]
rate change: Event,
reset: Event,
#[does_not_bubble]
resize: UiEvent,
#[does_not_bubble]
scroll: Event,
#[does_not_bubble]
scroll end: Event,
security policy violation: SecurityPolicyViolationEvent,
#[does_not_bubble]
seeked: Event,
#[does_not_bubble]
seeking: Event,
select: Event,
#[does_not_bubble]
selection change: Event,
select start: Event,
slot change: Event,
#[does_not_bubble]
stalled: Event,
submit: SubmitEvent,
#[does_not_bubble]
suspend: Event,
#[does_not_bubble]
time update: Event,
#[does_not_bubble]
toggle: Event,
touch cancel: TouchEvent,
touch end: TouchEvent,
touch move: TouchEvent,
touch start: TouchEvent,
transition cancel: TransitionEvent,
transition end: TransitionEvent,
transition run: TransitionEvent,
transition start: TransitionEvent,
#[does_not_bubble]
volume change: Event,
#[does_not_bubble]
waiting: Event,
webkit animation end: Event,
webkit animation iteration: Event,
webkit animation start: Event,
webkit transition end: Event,
wheel: WheelEvent,
// =========================================================
// WindowEventMap
// =========================================================
D O M Content Loaded: Event, // Hack for correct casing
#[does_not_bubble]
device motion: DeviceMotionEvent,
#[does_not_bubble]
device orientation: DeviceOrientationEvent,
#[does_not_bubble]
orientation change: Event,
// =========================================================
// DocumentAndElementEventHandlersEventMap
// =========================================================
copy: Event, // ClipboardEvent is unstable
cut: Event, // ClipboardEvent is unstable
paste: Event, // ClipboardEvent is unstable
// =========================================================
// DocumentEventMap
// =========================================================
fullscreen change: Event,
fullscreen error: Event,
pointer lock change: Event,
pointer lock error: Event,
#[does_not_bubble]
ready state change: Event,
visibility change: Event,
}
// Export `web_sys` event types
pub use web_sys::{
AnimationEvent, BeforeUnloadEvent, CompositionEvent, CustomEvent,
DeviceMotionEvent, DeviceOrientationEvent, DragEvent, ErrorEvent, Event,
FocusEvent, GamepadEvent, HashChangeEvent, InputEvent, KeyboardEvent,
MessageEvent, MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent,
ProgressEvent, PromiseRejectionEvent, SecurityPolicyViolationEvent,
StorageEvent, SubmitEvent, TouchEvent, TransitionEvent, UiEvent,
WheelEvent,
};

View File

@@ -141,7 +141,7 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
// Closure::once_into_js only frees the callback when it's actually
// called, so this instead uses into_js_value, which can be freed by
// the host JS engine's GC if it supports weak references (which all
// modern brower engines do). The way this works is that the provided
// modern browser engines do). The way this works is that the provided
// callback's captured data is dropped immediately after being called,
// as before, but it leaves behind a small stub closure rust-side that
// will be freed "eventually" by the JS GC. If the function is never
@@ -318,7 +318,7 @@ pub fn set_timeout_with_handle(
/// listeners to prevent them from firing constantly as you type.
///
/// ```
/// use leptos::{leptos_dom::helpers::debounce, logging::log, *};
/// use leptos::{leptos_dom::helpers::debounce, logging::log, prelude::*, *};
///
/// #[component]
/// fn DebouncedButton() -> impl IntoView {
@@ -507,7 +507,10 @@ pub fn window_event_listener_untyped(
/// Creates a window event listener from a typed event, returning a
/// cancelable handle.
/// ```
/// use leptos::{leptos_dom::helpers::window_event_listener, logging::log, *};
/// use leptos::{
/// ev, leptos_dom::helpers::window_event_listener, logging::log,
/// prelude::*,
/// };
///
/// #[component]
/// fn App() -> impl IntoView {

File diff suppressed because it is too large Load Diff

View File

@@ -1,312 +0,0 @@
#[cfg(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
))]
use leptos_reactive::SharedContext;
use std::{cell::RefCell, fmt::Display};
#[cfg(feature = "hydrate")]
mod hydrate_only {
use once_cell::unsync::Lazy as LazyCell;
use std::{cell::Cell, collections::HashMap};
use wasm_bindgen::JsCast;
/// See ["createTreeWalker"](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker)
#[allow(unused)]
const FILTER_SHOW_COMMENT: u32 = 0b10000000;
thread_local! {
pub static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
let document = crate::document();
let body = document.body().unwrap();
let walker = document
.create_tree_walker_with_what_to_show(&body, FILTER_SHOW_COMMENT)
.unwrap();
let mut map = HashMap::new();
while let Ok(Some(node)) = walker.next_node() {
if let Some(content) = node.text_content() {
if let Some(hk) = content.strip_prefix("hk=") {
if let Some(hk) = hk.split('|').next() {
map.insert(hk.into(), node.unchecked_into());
}
}
}
}
map
});
pub static HYDRATION_ELEMENTS: LazyCell<HashMap<String, web_sys::HtmlElement>> = LazyCell::new(|| {
let document = crate::document();
let els = document.query_selector_all("[data-hk]");
if let Ok(list) = els {
let len = list.length();
let mut map = HashMap::with_capacity(len as usize);
for idx in 0..len {
let el = list.item(idx).unwrap().unchecked_into::<web_sys::HtmlElement>();
let dataset = el.dataset();
let hk = dataset.get(wasm_bindgen::intern("hk")).unwrap();
map.insert(hk, el);
}
map
} else {
Default::default()
}
});
pub static IS_HYDRATING: Cell<bool> = const { Cell::new(true) };
}
#[allow(unused)]
pub fn get_marker(id: &str) -> Option<web_sys::Comment> {
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
}
#[allow(unused)]
pub fn get_element(hk: &str) -> Option<web_sys::HtmlElement> {
HYDRATION_ELEMENTS.with(|els| els.get(hk).cloned())
}
}
#[cfg(feature = "hydrate")]
pub(crate) use hydrate_only::*;
/// A stable identifier within the server-rendering or hydration process.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct HydrationKey {
/// ID of the current outlet
pub outlet: usize,
/// ID of the current fragment.
pub fragment: usize,
/// ID of the current error boundary.
pub error: usize,
/// ID of the current key.
pub id: usize,
}
impl Display for HydrationKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}-{}-{}-{}",
self.outlet, self.fragment, self.error, self.id
)
}
}
impl std::str::FromStr for HydrationKey {
type Err = (); // TODO better error
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut pieces = s.splitn(4, '-');
let first = pieces.next().ok_or(())?;
let second = pieces.next().ok_or(())?;
let third = pieces.next().ok_or(())?;
let fourth = pieces.next().ok_or(())?;
let outlet = usize::from_str(first).map_err(|_| ())?;
let fragment = usize::from_str(second).map_err(|_| ())?;
let error = usize::from_str(third).map_err(|_| ())?;
let id = usize::from_str(fourth).map_err(|_| ())?;
Ok(HydrationKey {
outlet,
fragment,
error,
id,
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn parse_hydration_key() {
use crate::HydrationKey;
use std::str::FromStr;
assert_eq!(
HydrationKey::from_str("0-1-2-3"),
Ok(HydrationKey {
outlet: 0,
fragment: 1,
error: 2,
id: 3
})
)
}
}
thread_local!(static ID: RefCell<HydrationKey> = const {RefCell::new(HydrationKey { outlet: 0, fragment: 0, error: 0, id: 0 })});
/// Control and utility methods for hydration.
pub struct HydrationCtx;
impl HydrationCtx {
/// If you're in an hydration context, get the next `id` without incrementing it.
pub fn peek() -> Option<HydrationKey> {
#[cfg(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
))]
let no_hydrate = SharedContext::no_hydrate();
#[cfg(not(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
)))]
let no_hydrate = false;
if no_hydrate {
None
} else {
Some(ID.with(|id| *id.borrow()))
}
}
/// Get the next `id` without incrementing it.
pub fn peek_always() -> HydrationKey {
ID.with(|id| *id.borrow())
}
/// Increments the current hydration `id` and returns it
pub fn id() -> Option<HydrationKey> {
#[cfg(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
))]
let no_hydrate = SharedContext::no_hydrate();
#[cfg(not(all(
feature = "experimental-islands",
any(feature = "hydrate", feature = "ssr")
)))]
let no_hydrate = false;
if no_hydrate {
None
} else {
Some(ID.with(|id| {
let mut id = id.borrow_mut();
id.id = id.id.wrapping_add(1);
*id
}))
}
}
/// Resets the hydration `id` for the next component, and returns it
pub fn next_component() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.fragment = id.fragment.wrapping_add(1);
id.id = 0;
*id
})
}
/// Resets the hydration `id` for the next outlet, and returns it
pub fn next_outlet() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.outlet = id.outlet.wrapping_add(1);
id.id = 0;
*id
})
}
/// Resets the hydration `id` for the next component, and returns it
pub fn next_error() -> HydrationKey {
ID.with(|id| {
let mut id = id.borrow_mut();
id.error = id.error.wrapping_add(1);
id.id = 0;
*id
})
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn reset_id() {
ID.with(|id| {
*id.borrow_mut() = HydrationKey {
outlet: 0,
fragment: 0,
error: 0,
id: 0,
}
});
}
/// Resumes hydration from the provided `id`. Useful for
/// `Suspense` and other fancy things.
pub fn continue_from(id: HydrationKey) {
ID.with(|i| *i.borrow_mut() = id);
}
/// Resumes hydration after the provided `id`. Useful for
/// islands and other fancy things.
pub fn continue_after(id: HydrationKey) {
ID.with(|i| {
*i.borrow_mut() = HydrationKey {
outlet: id.outlet,
fragment: id.fragment,
error: id.error,
id: id.id + 1,
}
});
}
#[doc(hidden)]
pub fn stop_hydrating() {
#[cfg(feature = "hydrate")]
{
IS_HYDRATING.with(|is_hydrating| {
is_hydrating.set(false);
})
}
}
#[doc(hidden)]
#[cfg(feature = "hydrate")]
pub fn with_hydration_on<T>(f: impl FnOnce() -> T) -> T {
let prev = IS_HYDRATING.with(|is_hydrating| {
let prev = is_hydrating.get();
is_hydrating.set(true);
prev
});
let value = f();
IS_HYDRATING.with(|is_hydrating| is_hydrating.set(prev));
value
}
#[doc(hidden)]
#[cfg(feature = "hydrate")]
pub fn with_hydration_off<T>(f: impl FnOnce() -> T) -> T {
let prev = IS_HYDRATING.with(|is_hydrating| {
let prev = is_hydrating.get();
is_hydrating.set(false);
prev
});
let value = f();
IS_HYDRATING.with(|is_hydrating| is_hydrating.set(prev));
value
}
/// Whether the UI is currently in the process of hydrating from the server-sent HTML.
#[inline(always)]
pub fn is_hydrating() -> bool {
#[cfg(feature = "hydrate")]
{
IS_HYDRATING.with(|is_hydrating| is_hydrating.get())
}
#[cfg(not(feature = "hydrate"))]
{
false
}
}
#[cfg(feature = "hydrate")]
#[allow(unused)]
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
#[cfg(debug_assertions)]
return format!("{id}{}", if closing { 'c' } else { 'o' });
#[cfg(not(debug_assertions))]
{
id.to_string()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,421 +0,0 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use leptos_reactive::{Oco, TextProp};
use std::{borrow::Cow, rc::Rc};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
/// Represents the different possible values an attribute node could have.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
#[derive(Clone)]
pub enum Attribute {
/// A plain string value.
String(Oco<'static, str>),
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
Fn(Rc<dyn Fn() -> Attribute>),
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
Option(Option<Oco<'static, str>>),
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
Bool(bool),
}
impl Attribute {
/// Converts the attribute to its HTML value at that moment, including the attribute name,
/// so it can be rendered on the server.
pub fn as_value_string(
&self,
attr_name: &'static str,
) -> Oco<'static, str> {
match self {
Attribute::String(value) => {
format!("{attr_name}=\"{value}\"").into()
}
Attribute::Fn(f) => {
let mut value = f();
while let Attribute::Fn(f) = value {
value = f();
}
value.as_value_string(attr_name)
}
Attribute::Option(value) => value
.as_ref()
.map(|value| format!("{attr_name}=\"{value}\"").into())
.unwrap_or_default(),
Attribute::Bool(include) => {
Oco::Borrowed(if *include { attr_name } else { "" })
}
}
}
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> Option<Oco<'static, str>> {
match self {
Attribute::String(value) => Some(value.clone()),
Attribute::Fn(f) => {
let mut value = f();
while let Attribute::Fn(f) = value {
value = f();
}
value.as_nameless_value_string()
}
Attribute::Option(value) => value.as_ref().cloned(),
Attribute::Bool(include) => {
if *include {
Some("".into())
} else {
None
}
}
}
}
}
impl PartialEq for Attribute {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::String(l0), Self::String(r0)) => l0 == r0,
(Self::Fn(_), Self::Fn(_)) => false,
(Self::Option(l0), Self::Option(r0)) => l0 == r0,
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
_ => false,
}
}
}
impl core::fmt::Debug for Attribute {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
Self::Fn(_) => f.debug_tuple("Fn").finish(),
Self::Option(arg0) => f.debug_tuple("Option").field(arg0).finish(),
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
}
}
}
/// Converts some type into an [`Attribute`].
///
/// This is implemented by default for Rust primitive and string types.
pub trait IntoAttribute {
/// Converts the object into an [`Attribute`].
fn into_attribute(self) -> Attribute;
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
fn into_attribute_boxed(self: Box<Self>) -> Attribute;
}
impl<T: IntoAttribute + 'static> From<T> for Box<dyn IntoAttribute> {
#[inline(always)]
fn from(value: T) -> Self {
Box::new(value)
}
}
impl IntoAttribute for Attribute {
#[inline(always)]
fn into_attribute(self) -> Attribute {
self
}
#[inline(always)]
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
*self
}
}
macro_rules! impl_into_attr_boxed {
() => {
#[inline(always)]
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
self.into_attribute()
}
};
}
impl IntoAttribute for String {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Owned(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for &'static str {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Borrowed(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Cow<'static, str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(self.into())
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Oco<'static, str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(self)
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Rc<str> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::String(Oco::Counted(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for bool {
#[inline(always)]
fn into_attribute(self) -> Attribute {
Attribute::Bool(self)
}
impl_into_attr_boxed! {}
}
impl<T: IntoAttribute> IntoAttribute for Option<T> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
self.map_or(Attribute::Option(None), IntoAttribute::into_attribute)
}
impl_into_attr_boxed! {}
}
impl<T, U> IntoAttribute for T
where
T: Fn() -> U + 'static,
U: IntoAttribute,
{
fn into_attribute(self) -> Attribute {
let modified_fn = Rc::new(move || (self)().into_attribute());
Attribute::Fn(modified_fn)
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<Box<dyn IntoAttribute>> {
fn into_attribute(self) -> Attribute {
match self {
Some(bx) => bx.into_attribute_boxed(),
None => Attribute::Option(None),
}
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for TextProp {
fn into_attribute(self) -> Attribute {
(move || self.get()).into_attribute()
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for core::fmt::Arguments<'_> {
fn into_attribute(self) -> Attribute {
match self.as_str() {
Some(s) => s.into_attribute(),
None => self.to_string().into_attribute(),
}
}
impl_into_attr_boxed! {}
}
/* impl IntoAttribute for Box<dyn IntoAttribute> {
#[inline(always)]
fn into_attribute(self) -> Attribute {
self.into_attribute_boxed()
}
impl_into_attr_boxed! {}
} */
macro_rules! attr_type {
($attr_type:ty) => {
impl IntoAttribute for $attr_type {
fn into_attribute(self) -> Attribute {
Attribute::String(self.to_string().into())
}
#[inline]
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
self.into_attribute()
}
}
};
}
macro_rules! attr_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoAttribute for $signal_type
where
T: IntoAttribute + Clone,
{
fn into_attribute(self) -> Attribute {
let modified_fn = Rc::new(move || self.get().into_attribute());
Attribute::Fn(modified_fn)
}
impl_into_attr_boxed! {}
}
};
}
attr_type!(&String);
attr_type!(usize);
attr_type!(u8);
attr_type!(u16);
attr_type!(u32);
attr_type!(u64);
attr_type!(u128);
attr_type!(isize);
attr_type!(i8);
attr_type!(i16);
attr_type!(i32);
attr_type!(i64);
attr_type!(i128);
attr_type!(f32);
attr_type!(f64);
attr_type!(char);
attr_signal_type!(ReadSignal<T>);
attr_signal_type!(RwSignal<T>);
attr_signal_type!(Memo<T>);
attr_signal_type!(Signal<T>);
attr_signal_type!(MaybeSignal<T>);
attr_signal_type!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
#[track_caller]
pub fn attribute_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Attribute,
) {
#[cfg(debug_assertions)]
let called_at = std::panic::Location::caller();
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(f) => {
let el = el.clone();
create_render_effect(move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(
&el,
&name,
new.clone(),
true,
#[cfg(debug_assertions)]
called_at,
);
}
new
});
}
_ => attribute_expression(
el,
&name,
value,
false,
#[cfg(debug_assertions)]
called_at,
),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,
value: Attribute,
force: bool,
#[cfg(debug_assertions)] called_at: &'static std::panic::Location<'static>,
) {
use crate::HydrationCtx;
if force || !HydrationCtx::is_hydrating() {
match value {
Attribute::String(value) => {
let value = wasm_bindgen::intern(&value);
if attr_name == "inner_html" {
el.set_inner_html(value);
} else {
let attr_name = wasm_bindgen::intern(attr_name);
el.set_attribute(attr_name, value).unwrap_throw();
}
}
Attribute::Option(value) => {
if attr_name == "inner_html" {
el.set_inner_html(&value.unwrap_or_default());
} else {
let attr_name = wasm_bindgen::intern(attr_name);
match value {
Some(value) => {
let value = wasm_bindgen::intern(&value);
el.set_attribute(attr_name, value).unwrap_throw();
}
None => el.remove_attribute(attr_name).unwrap_throw(),
}
}
}
Attribute::Bool(value) => {
let attr_name = wasm_bindgen::intern(attr_name);
if value {
el.set_attribute(attr_name, attr_name).unwrap_throw();
} else {
el.remove_attribute(attr_name).unwrap_throw();
}
}
Attribute::Fn(f) => {
let mut v = f();
crate::debug_warn!(
"At {called_at}, you are providing a dynamic attribute \
with a nested function. For example, you might have a \
closure that returns another function instead of a \
value. This creates some added overhead. If possible, \
you should instead provide a function that returns a \
value instead.",
);
while let Attribute::Fn(f) = v {
v = f();
}
attribute_expression(
el,
attr_name,
v,
force,
#[cfg(debug_assertions)]
called_at,
);
}
}
}
}

View File

@@ -1,172 +0,0 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
/// Represents the different possible values a single class on an element could have,
/// allowing you to do fine-grained updates to single items
/// in [`Element.classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
pub enum Class {
/// Whether the class is present.
Value(bool),
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
Fn(Box<dyn Fn() -> bool>),
}
/// Converts some type into a [`Class`].
pub trait IntoClass {
/// Converts the object into a [`Class`].
fn into_class(self) -> Class;
/// Helper function for dealing with `Box<dyn IntoClass>`.
fn into_class_boxed(self: Box<Self>) -> Class;
}
impl IntoClass for bool {
#[inline(always)]
fn into_class(self) -> Class {
Class::Value(self)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
impl<T> IntoClass for T
where
T: Fn() -> bool + 'static,
{
#[inline(always)]
fn into_class(self) -> Class {
let modified_fn = Box::new(self);
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
impl Class {
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
match self {
Class::Value(value) => {
if *value {
class_name
} else {
""
}
}
Class::Fn(f) => {
let value = f();
if value {
class_name
} else {
""
}
}
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
pub fn class_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Class,
) {
use leptos_reactive::create_render_effect;
let class_list = el.class_list();
match value {
Class::Fn(f) => {
create_render_effect(move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new, true)
}
new
});
}
Class::Value(value) => {
class_expression(&class_list, &name, value, false)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn class_expression(
class_list: &web_sys::DomTokenList,
class_name: &str,
value: bool,
force: bool,
) {
use crate::HydrationCtx;
if force || !HydrationCtx::is_hydrating() {
let class_name = wasm_bindgen::intern(class_name);
if value {
if let Err(e) = class_list.add_1(class_name) {
crate::error!("[HtmlElement::class()] {e:?}");
}
} else {
if let Err(e) = class_list.remove_1(class_name) {
crate::error!("[HtmlElement::class()] {e:?}");
}
}
}
}
macro_rules! class_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl IntoClass for $signal_type {
#[inline(always)]
fn into_class(self) -> Class {
let modified_fn = Box::new(move || self.get());
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
};
}
macro_rules! class_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl IntoClass for $signal_type {
#[inline(always)]
fn into_class(self) -> Class {
let modified_fn = Box::new(move || self.get().unwrap_or(false));
Class::Fn(modified_fn)
}
fn into_class_boxed(self: Box<Self>) -> Class {
(*self).into_class()
}
}
};
}
class_signal_type!(ReadSignal<bool>);
class_signal_type!(RwSignal<bool>);
class_signal_type!(Memo<bool>);
class_signal_type!(Signal<bool>);
class_signal_type!(MaybeSignal<bool>);
class_signal_type_optional!(MaybeProp<bool>);

View File

@@ -1,178 +0,0 @@
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use wasm_bindgen::JsValue;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
/// Represents the different possible values an element property could have,
/// allowing you to do fine-grained updates to single fields.
///
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
/// macros use. You usually won't need to interact with it directly, but it can be useful for defining
/// permissive APIs for certain components.
pub enum Property {
/// A static JavaScript value.
Value(JsValue),
/// A (presumably reactive) function, which will be run inside an effect to update the property.
Fn(Box<dyn Fn() -> JsValue>),
}
/// Converts some type into a [`Property`].
///
/// This is implemented by default for Rust primitive types, [`String`] and friends, and [`JsValue`].
pub trait IntoProperty {
/// Converts the object into a [`Property`].
fn into_property(self) -> Property;
/// Helper function for dealing with `Box<dyn IntoProperty>`.
fn into_property_boxed(self: Box<Self>) -> Property;
}
impl<T, U> IntoProperty for T
where
T: Fn() -> U + 'static,
U: Into<JsValue>,
{
fn into_property(self) -> Property {
let modified_fn = Box::new(move || self().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
macro_rules! prop_type {
($prop_type:ty) => {
impl IntoProperty for $prop_type {
#[inline(always)]
fn into_property(self) -> Property {
Property::Value(self.into())
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
impl IntoProperty for Option<$prop_type> {
#[inline(always)]
fn into_property(self) -> Property {
Property::Value(self.into())
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
macro_rules! prop_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoProperty for $signal_type
where
T: Into<JsValue> + Clone,
{
fn into_property(self) -> Property {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
macro_rules! prop_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoProperty for $signal_type
where
T: Clone,
Option<T>: Into<JsValue>,
{
fn into_property(self) -> Property {
let modified_fn = Box::new(move || self.get().into());
Property::Fn(modified_fn)
}
fn into_property_boxed(self: Box<Self>) -> Property {
(*self).into_property()
}
}
};
}
prop_type!(JsValue);
prop_type!(String);
prop_type!(&String);
prop_type!(&str);
prop_type!(usize);
prop_type!(u8);
prop_type!(u16);
prop_type!(u32);
prop_type!(u64);
prop_type!(u128);
prop_type!(isize);
prop_type!(i8);
prop_type!(i16);
prop_type!(i32);
prop_type!(i64);
prop_type!(i128);
prop_type!(f32);
prop_type!(f64);
prop_type!(bool);
prop_signal_type!(ReadSignal<T>);
prop_signal_type!(RwSignal<T>);
prop_signal_type!(Memo<T>);
prop_signal_type!(Signal<T>);
prop_signal_type!(MaybeSignal<T>);
prop_signal_type_optional!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn property_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Property,
) {
use leptos_reactive::create_render_effect;
match value {
Property::Fn(f) => {
let el = el.clone();
create_render_effect(move |_| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
property_expression(&el, prop_name, new.clone());
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn property_expression(
el: &web_sys::Element,
prop_name: &str,
value: JsValue,
) {
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
.unwrap_throw();
}

View File

@@ -1,360 +0,0 @@
use leptos_reactive::Oco;
#[cfg(not(feature = "nightly"))]
use leptos_reactive::{
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
};
use std::{borrow::Cow, rc::Rc};
/// todo docs
#[derive(Clone)]
pub enum Style {
/// A plain string value.
Value(Oco<'static, str>),
/// An optional string value, which sets the property to the value if `Some` and removes the property if `None`.
Option(Option<Oco<'static, str>>),
/// A (presumably reactive) function, which will be run inside an effect to update the style.
Fn(Rc<dyn Fn() -> Style>),
}
impl PartialEq for Style {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Value(l0), Self::Value(r0)) => l0 == r0,
(Self::Fn(_), Self::Fn(_)) => false,
(Self::Option(l0), Self::Option(r0)) => l0 == r0,
_ => false,
}
}
}
impl core::fmt::Debug for Style {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Value(arg0) => f.debug_tuple("Value").field(arg0).finish(),
Self::Fn(_) => f.debug_tuple("Fn").finish(),
Self::Option(arg0) => f.debug_tuple("Option").field(arg0).finish(),
}
}
}
/// Converts some type into a [`Style`].
pub trait IntoStyle {
/// Converts the object into a [`Style`].
fn into_style(self) -> Style;
/// Helper function for dealing with `Box<dyn IntoStyle>`.
fn into_style_boxed(self: Box<Self>) -> Style;
}
impl IntoStyle for Style {
fn into_style(self) -> Style {
self
}
fn into_style_boxed(self: Box<Self>) -> Style {
*self
}
}
impl IntoStyle for &'static str {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(Oco::Borrowed(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for String {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(Oco::Owned(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Rc<str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(Oco::Counted(self))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(self).into_style()
}
}
impl IntoStyle for Cow<'static, str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(self.into())
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Oco<'static, str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Value(self)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<&'static str> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Borrowed))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<String> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Owned))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(self).into_style()
}
}
impl IntoStyle for Option<Rc<str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::Counted))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<Cow<'static, str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self.map(Oco::from))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<Oco<'static, str>> {
#[inline(always)]
fn into_style(self) -> Style {
Style::Option(self)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl<T, U> IntoStyle for T
where
T: Fn() -> U + 'static,
U: IntoStyle,
{
#[inline(always)]
fn into_style(self) -> Style {
let modified_fn = Rc::new(move || (self)().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl Style {
/// Converts the style to its HTML value at that moment so it can be rendered on the server.
pub fn as_value_string(
&self,
style_name: &str,
) -> Option<Oco<'static, str>> {
match self {
Style::Value(value) => {
Some(format!("{style_name}: {value};").into())
}
Style::Option(value) => value
.as_ref()
.map(|value| format!("{style_name}: {value};").into()),
Style::Fn(f) => {
let mut value = f();
while let Style::Fn(f) = value {
value = f();
}
value.as_value_string(style_name)
}
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
pub fn style_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Style,
) {
use leptos_reactive::create_render_effect;
use std::ops::Deref;
use wasm_bindgen::JsCast;
let el = el.unchecked_ref::<web_sys::HtmlElement>();
let style_list = el.style();
match value {
Style::Fn(f) => {
create_render_effect(move |old| {
let mut new = f();
while let Style::Fn(f) = new {
new = f();
}
let new = match new {
Style::Value(value) => Some(value),
Style::Option(value) => value,
_ => unreachable!(),
};
if old.as_ref() != Some(&new) {
style_expression(&style_list, &name, new.as_deref(), true)
}
new
});
}
Style::Value(value) => {
style_expression(&style_list, &name, Some(value.deref()), false)
}
Style::Option(value) => {
style_expression(&style_list, &name, value.as_deref(), false)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn style_expression(
style_list: &web_sys::CssStyleDeclaration,
style_name: &str,
value: Option<&str>,
force: bool,
) {
use crate::HydrationCtx;
if force || !HydrationCtx::is_hydrating() {
let style_name = wasm_bindgen::intern(style_name);
if let Some(value) = value {
if let Err(e) = style_list.set_property(style_name, value) {
crate::error!("[HtmlElement::style()] {e:?}");
}
} else {
if let Err(e) = style_list.remove_property(style_name) {
crate::error!("[HtmlElement::style()] {e:?}");
}
}
}
}
macro_rules! style_type {
($style_type:ty) => {
impl IntoStyle for $style_type {
fn into_style(self) -> Style {
Style::Value(self.to_string().into())
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
impl IntoStyle for Option<$style_type> {
fn into_style(self) -> Style {
Style::Option(self.map(|n| n.to_string().into()))
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}
macro_rules! style_signal_type {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoStyle for $signal_type
where
T: IntoStyle + Clone,
{
fn into_style(self) -> Style {
let modified_fn = Rc::new(move || self.get().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}
macro_rules! style_signal_type_optional {
($signal_type:ty) => {
#[cfg(not(feature = "nightly"))]
impl<T> IntoStyle for $signal_type
where
T: Clone,
Option<T>: IntoStyle,
{
fn into_style(self) -> Style {
let modified_fn = Rc::new(move || self.get().into_style());
Style::Fn(modified_fn)
}
fn into_style_boxed(self: Box<Self>) -> Style {
(*self).into_style()
}
}
};
}
style_type!(&String);
style_type!(usize);
style_type!(u8);
style_type!(u16);
style_type!(u32);
style_type!(u64);
style_type!(u128);
style_type!(isize);
style_type!(i8);
style_type!(i16);
style_type!(i32);
style_type!(i64);
style_type!(i128);
style_type!(f32);
style_type!(f64);
style_type!(char);
style_signal_type!(ReadSignal<T>);
style_signal_type!(RwSignal<T>);
style_signal_type!(Memo<T>);
style_signal_type!(Signal<T>);
style_signal_type!(MaybeSignal<T>);
style_signal_type_optional!(MaybeProp<T>);

View File

@@ -1,11 +0,0 @@
mod into_attribute;
mod into_class;
mod into_property;
mod into_style;
#[cfg(feature = "trace-component-props")]
#[doc(hidden)]
pub mod tracing_property;
pub use into_attribute::*;
pub use into_class::*;
pub use into_property::*;
pub use into_style::*;

View File

@@ -1,176 +0,0 @@
use wasm_bindgen::UnwrapThrowExt;
#[macro_export]
/// Use for tracing property
macro_rules! tracing_props {
() => {
::leptos::leptos_dom::tracing::span!(
::leptos::leptos_dom::tracing::Level::TRACE,
"leptos_dom::tracing_props",
props = String::from("[]")
);
};
($($prop:tt),+ $(,)?) => {
{
use ::leptos::leptos_dom::tracing_property::{Match, SerializeMatch, DefaultMatch};
let mut props = String::from('[');
$(
let prop = (&&Match {
name: stringify!{$prop},
value: std::cell::Cell::new(Some(&$prop))
}).spez();
props.push_str(&format!("{prop},"));
)*
props.pop();
props.push(']');
::leptos::leptos_dom::tracing::span!(
::leptos::leptos_dom::tracing::Level::TRACE,
"leptos_dom::tracing_props",
props
);
}
};
}
// Implementation based on spez
// see https://github.com/m-ou-se/spez
pub struct Match<T> {
pub name: &'static str,
pub value: std::cell::Cell<Option<T>>,
}
pub trait SerializeMatch {
type Return;
fn spez(&self) -> Self::Return;
}
impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
// suppresses warnings when serializing signals into props
#[cfg(debug_assertions)]
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
let value = serde_json::to_string(self.value.get().unwrap_throw())
.map_or_else(
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
);
#[cfg(debug_assertions)]
leptos_reactive::SpecialNonReactiveZone::exit(prev);
value
}
}
pub trait DefaultMatch {
type Return;
fn spez(&self) -> Self::Return;
}
impl<T> DefaultMatch for Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
}
}
#[test]
fn match_primitive() {
// String
let test = String::from("string");
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// &str
let test = "string";
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// u128
let test: u128 = 1;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": 1}"#);
// i128
let test: i128 = -1;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": -1}"#);
// f64
let test = 3.25;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": 3.25}"#);
// bool
let test = true;
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": true}"#);
}
#[test]
fn match_serialize() {
use serde::Serialize;
#[derive(Serialize)]
struct CustomStruct {
field: &'static str,
}
let test = CustomStruct { field: "field" };
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": {"field":"field"}}"#);
// Verification of ownership
assert_eq!(test.field, "field");
}
#[test]
#[allow(clippy::needless_borrow)]
fn match_no_serialize() {
struct CustomStruct {
field: &'static str,
}
let test = CustomStruct { field: "field" };
let prop = (&&Match {
name: stringify! {test},
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(
prop,
r#"{"name": "test", "value": "[unserializable value]"}"#
);
// Verification of ownership
assert_eq!(test.field, "field");
}

View File

@@ -1,238 +0,0 @@
//! Exports types for working with MathML elements.
use super::{AnyElement, ElementDescriptor, HtmlElement};
use crate::HydrationCtx;
use cfg_if::cfg_if;
use leptos_reactive::Oco;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use once_cell::unsync::Lazy as LazyCell;
use wasm_bindgen::JsCast;
} else {
use super::{HydrationKey, html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
}
}
macro_rules! generate_math_tags {
(
$(
#[$meta:meta]
$(#[$void:ident])?
$tag:ident $(- $second:ident $(- $third:ident)?)? $(@ $trailing_:pat)?
),* $(,)?
) => {
paste::paste! {
$(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
thread_local! {
static [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]: LazyCell<web_sys::HtmlElement> = LazyCell::new(|| {
crate::document()
.create_element_ns(
Some(wasm_bindgen::intern("http://www.w3.org/1998/Math/MathML")),
concat![
stringify!($tag),
$(
"-", stringify!($second),
$(
"-", stringify!($third)
)?
)?
],
)
.unwrap()
.unchecked_into()
});
}
#[derive(Clone, Debug)]
#[$meta]
pub struct [<$tag:camel $($second:camel $($third:camel)?)?>] {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: web_sys::HtmlElement,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id: Option<HydrationKey>,
}
impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn default() -> Self {
#[allow(unused)]
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
let element = if HydrationCtx::is_hydrating() && id.is_some() {
let id = id.unwrap();
if let Some(el) = crate::hydration::get_element(&id.to_string()) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
);
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
} else {
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
};
#[cfg(all(target_arch = "wasm32", feature = "web", not(feature = "hydrate")))]
let element = [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
);
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id
}
}
}
impl std::ops::Deref for [<$tag:camel $($second:camel $($third:camel)?)?>] {
type Target = web_sys::Element;
fn deref(&self) -> &Self::Target {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
use wasm_bindgen::JsCast;
return &self.element.unchecked_ref();
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}");
}
}
impl std::convert::AsRef<web_sys::HtmlElement> for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn as_ref(&self) -> &web_sys::HtmlElement {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return &self.element;
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}");
}
}
impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn name(&self) -> Oco<'static, str> {
stringify!($tag).into()
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> &Option<HydrationKey> {
&self.id
}
generate_math_tags! { @void $($void)? }
}
impl From<HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>> for HtmlElement<AnyElement> {
fn from(element: HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>) -> Self {
element.into_any()
}
}
#[$meta]
pub fn [<$tag $(_ $second $(_ $third)?)? $($trailing_)?>]() -> HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]> {
HtmlElement::new([<$tag:camel $($second:camel $($third:camel)?)?>]::default())
}
)*
}
};
(@void) => {};
(@void void) => {
fn is_void(&self) -> bool {
true
}
};
}
generate_math_tags![
/// MathML element.
math,
/// MathML element.
mi,
/// MathML element.
mn,
/// MathML element.
mo,
/// MathML element.
ms,
/// MathML element.
mspace,
/// MathML element.
mtext,
/// MathML element.
menclose,
/// MathML element.
merror,
/// MathML element.
mfenced,
/// MathML element.
mfrac,
/// MathML element.
mpadded,
/// MathML element.
mphantom,
/// MathML element.
mroot,
/// MathML element.
mrow,
/// MathML element.
msqrt,
/// MathML element.
mstyle,
/// MathML element.
mmultiscripts,
/// MathML element.
mover,
/// MathML element.
mprescripts,
/// MathML element.
msub,
/// MathML element.
msubsup,
/// MathML element.
msup,
/// MathML element.
munder,
/// MathML element.
munderover,
/// MathML element.
mtable,
/// MathML element.
mtd,
/// MathML element.
mtr,
/// MathML element.
maction,
/// MathML element.
annotation,
/// MathML element.
annotation
- xml,
/// MathML element.
semantics,
];

View File

@@ -1,226 +0,0 @@
use crate::{html::ElementDescriptor, HtmlElement};
use leptos_reactive::{create_render_effect, signal_prelude::*};
use std::cell::Cell;
/// Contains a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
///
/// ```
/// # use leptos::{*, logging::log};
/// use leptos::html::Input;
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// let input_ref = create_node_ref::<Input>();
///
/// let on_click = move |_| {
/// let node =
/// input_ref.get().expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///
/// view! {
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// }
/// ```
#[cfg_attr(not(debug_assertions), repr(transparent))]
pub struct NodeRef<T: ElementDescriptor + 'static> {
element: RwSignal<Option<HtmlElement<T>>>,
#[cfg(debug_assertions)]
defined_at: &'static std::panic::Location<'static>,
}
/// Creates a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
///
/// ```
/// # use leptos::{*, logging::log};
/// use leptos::html::Input;
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// let input_ref = create_node_ref::<Input>();
///
/// let on_click = move |_| {
/// let node =
/// input_ref.get().expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///
/// view! {
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// }
/// ```
#[track_caller]
#[inline(always)]
pub fn create_node_ref<T: ElementDescriptor + 'static>() -> NodeRef<T> {
NodeRef {
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
element: create_rw_signal(None),
}
}
impl<T: ElementDescriptor + 'static> NodeRef<T> {
/// Creates a shared reference to a DOM node created while using the `view`
/// macro to create your UI.
///
/// This is identical to [`create_node_ref`].
///
/// ```
/// # use leptos::{*, logging::log};
///
/// use leptos::html::Input;
///
/// #[component]
/// pub fn MyComponent() -> impl IntoView {
/// let input_ref = NodeRef::<Input>::new();
///
/// let on_click = move |_| {
/// let node =
/// input_ref.get().expect("input_ref should be loaded by now");
/// // `node` is strongly typed
/// // it is dereferenced to an `HtmlInputElement` automatically
/// log!("value is {:?}", node.value())
/// };
///
/// view! {
/// <div>
/// // `node_ref` loads the input
/// <input _ref=input_ref type="text"/>
/// // the button consumes it
/// <button on:click=on_click>"Click me"</button>
/// </div>
/// }
/// }
/// ```
#[inline(always)]
#[track_caller]
pub fn new() -> Self {
create_node_ref()
}
/// Gets the element that is currently stored in the reference.
///
/// This tracks reactively, so that node references can be used in effects.
/// Initially, the value will be `None`, but once it is loaded the effect
/// will rerun and its value will be `Some(Element)`.
#[track_caller]
#[inline(always)]
pub fn get(&self) -> Option<HtmlElement<T>>
where
T: Clone,
{
self.element.get()
}
/// Gets the element that is currently stored in the reference.
///
/// This **does not** track reactively.
#[track_caller]
#[inline(always)]
pub fn get_untracked(&self) -> Option<HtmlElement<T>>
where
T: Clone,
{
self.element.get_untracked()
}
#[doc(hidden)]
/// Loads an element into the reference. This tracks reactively,
/// so that effects that use the node reference will rerun once it is loaded,
/// i.e., effects can be forward-declared.
#[track_caller]
pub fn load(&self, node: &HtmlElement<T>)
where
T: Clone,
{
self.element.update(|current| {
if current.is_some() {
#[cfg(debug_assertions)]
crate::debug_warn!(
"You are setting the NodeRef defined at {}, which has \
already been filled Its possible this is intentional, \
but its also possible that youre accidentally using \
the same NodeRef for multiple _ref attributes.",
self.defined_at
);
}
*current = Some(node.clone());
});
}
/// Runs the provided closure when the `NodeRef` has been connected
/// with it's [`HtmlElement`].
#[inline(always)]
pub fn on_load<F>(self, f: F)
where
T: Clone,
F: FnOnce(HtmlElement<T>) + 'static,
{
let f = Cell::new(Some(f));
create_render_effect(move |_| {
if let Some(node_ref) = self.get() {
f.take().unwrap()(node_ref);
}
});
}
}
impl<T: ElementDescriptor> Clone for NodeRef<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
impl<T: ElementDescriptor + 'static> Default for NodeRef<T> {
fn default() -> Self {
Self::new()
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<HtmlElement<T>>;
#[inline(always)]
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T: Clone + ElementDescriptor + 'static> FnMut<()> for NodeRef<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get()
}
}
impl<T: Clone + ElementDescriptor + Clone + 'static> Fn<()> for NodeRef<T> {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get()
}
}
}
}

View File

@@ -1,148 +0,0 @@
use crate::{Attribute, IntoAttribute};
use leptos_reactive::use_context;
use std::{fmt::Display, ops::Deref};
/// A nonce a cryptographic nonce ("number used once") which can be
/// used by Content Security Policy to determine whether or not a given
/// resource will be allowed to load.
///
/// When the `nonce` feature is enabled on one of the server integrations,
/// a nonce is generated during server rendering and added to all inline
/// scripts used for HTML streaming and resource loading.
///
/// The nonce being used during the current server response can be
/// accessed using [`use_nonce`].
///
/// ```rust,ignore
/// #[component]
/// pub fn App() -> impl IntoView {
/// provide_meta_context;
///
/// view! {
/// // use `leptos_meta` to insert a <meta> tag with the CSP
/// <Meta
/// http_equiv="Content-Security-Policy"
/// content=move || {
/// // this will insert the CSP with nonce on the server, be empty on client
/// use_nonce()
/// .map(|nonce| {
/// format!(
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
/// )
/// })
/// .unwrap_or_default()
/// }
/// />
/// // manually insert nonce during SSR on inline script
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
/// <Style>"body { color: blue; }"</Style>
/// <p>"Test"</p>
/// }
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Nonce(pub(crate) String);
impl Deref for Nonce {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Nonce {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
impl IntoAttribute for Nonce {
fn into_attribute(self) -> Attribute {
Attribute::String(self.0.into())
}
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
Attribute::String(self.0.into())
}
}
/// Accesses the nonce that has been generated during the current
/// server response. This can be added to inline `<script>` and
/// `<style>` tags for compatibility with a Content Security Policy.
///
/// ```rust,ignore
/// #[component]
/// pub fn App() -> impl IntoView {
/// provide_meta_context;
///
/// view! {
/// // use `leptos_meta` to insert a <meta> tag with the CSP
/// <Meta
/// http_equiv="Content-Security-Policy"
/// content=move || {
/// // this will insert the CSP with nonce on the server, be empty on client
/// use_nonce()
/// .map(|nonce| {
/// format!(
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
/// )
/// })
/// .unwrap_or_default()
/// }
/// />
/// // manually insert nonce during SSR on inline script
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
/// <Style>"body { color: blue; }"</Style>
/// <p>"Test"</p>
/// }
/// }
/// ```
pub fn use_nonce() -> Option<Nonce> {
use_context::<Nonce>()
}
#[cfg(all(feature = "ssr", feature = "nonce"))]
pub use generate::*;
#[cfg(all(feature = "ssr", feature = "nonce"))]
mod generate {
use super::Nonce;
use base64::{
alphabet,
engine::{self, general_purpose},
Engine,
};
use leptos_reactive::provide_context;
use rand::{thread_rng, RngCore};
const NONCE_ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(
&alphabet::URL_SAFE,
general_purpose::NO_PAD,
);
impl Nonce {
/// Generates a new nonce from 16 bytes (128 bits) of random data.
pub fn new() -> Self {
let mut thread_rng = thread_rng();
let mut bytes = [0; 16];
thread_rng.fill_bytes(&mut bytes);
Nonce(NONCE_ENGINE.encode(bytes))
}
}
impl Default for Nonce {
fn default() -> Self {
Self::new()
}
}
/// Generates a nonce and provides it during server rendering.
pub fn provide_nonce() {
provide_context(Nonce::new())
}
}

View File

@@ -1,789 +0,0 @@
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
//! Server-side HTML rendering utilities.
use crate::{
html::{ElementChildren, StringOrView},
CoreComponent, HydrationCtx, HydrationKey, IntoView, View,
};
use cfg_if::cfg_if;
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
use itertools::Itertools;
use leptos_reactive::*;
use std::pin::Pin;
type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
/// Renders the given function to a static HTML string.
///
/// ```
/// # cfg_if::cfg_if! { if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// # use leptos::*;
/// let html = leptos::ssr::render_to_string(|| view! {
/// <p>"Hello, world!"</p>
/// });
/// // trim off the beginning, which has a bunch of hydration info, for comparison
/// assert!(html.contains("Hello, world!</p>"));
/// # }}
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn render_to_string<F, N>(f: F) -> Oco<'static, str>
where
F: FnOnce() -> N + 'static,
N: IntoView,
{
HydrationCtx::reset_id();
let runtime = leptos_reactive::create_runtime();
let html = f().into_view().render_to_string();
runtime.dispose();
html
}
/// Renders a function to a stream of HTML strings.
///
/// This renders:
/// 1) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 2) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 3) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn render_to_stream(
view: impl FnOnce() -> View + 'static,
) -> impl Stream<Item = String> {
render_to_stream_with_prefix(view, || "".into())
}
/// Renders a function to a stream of HTML strings. After the `view` runs, the `prefix` will run with
/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph.
///
/// This renders:
/// 1) the prefix
/// 2) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn render_to_stream_with_prefix(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
) -> impl Stream<Item = String> {
let (stream, runtime) =
render_to_stream_with_prefix_undisposed(view, prefix);
runtime.dispose();
stream
}
/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so
/// it can be disposed when appropriate. After the `view` runs, the `prefix` will run with
/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph.
///
/// This renders:
/// 1) the prefix
/// 2) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn render_to_stream_with_prefix_undisposed(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
) -> (impl Stream<Item = String>, RuntimeId) {
render_to_stream_with_prefix_undisposed_with_context(view, prefix, || {})
}
/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph.
///
/// This renders:
/// 1) the prefix
/// 2) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn render_to_stream_with_prefix_undisposed_with_context(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
additional_context: impl FnOnce() + 'static,
) -> (impl Stream<Item = String>, RuntimeId) {
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
view,
prefix,
additional_context,
false,
)
}
/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph.
///
/// If `replace_blocks` is true, this will wait for any fragments with blocking resources and
/// actually replace them in the initial HTML. This is slower to render (as it requires walking
/// back over the HTML for string replacement) but has the advantage of never including those fallbacks
/// in the HTML.
///
/// This renders:
/// 1) the prefix
/// 2) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
additional_context: impl FnOnce() + 'static,
replace_blocks: bool,
) -> (impl Stream<Item = String>, RuntimeId) {
HydrationCtx::reset_id();
// create the runtime
let runtime = create_runtime();
// Add additional context items
additional_context();
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
let shell = view().render_to_string();
let resources = SharedContext::pending_resources();
let pending_resources = serde_json::to_string(&resources).unwrap();
let pending_fragments = SharedContext::pending_fragments();
let serializers = SharedContext::serialization_resolvers();
let nonce_str = crate::nonce::use_nonce()
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
let local_only = SharedContext::fragments_with_local_resources();
let local_only = serde_json::to_string(&local_only).unwrap();
let mut blocking_fragments = FuturesUnordered::new();
let fragments = FuturesUnordered::new();
for (fragment_id, data) in pending_fragments {
if data.should_block {
blocking_fragments
.push(async move { (fragment_id, data.out_of_order.await) });
} else {
fragments.push(Box::pin(async move {
(fragment_id, data.out_of_order.await)
})
as Pin<Box<dyn Future<Output = (String, String)>>>);
}
}
let stream = futures::stream::once(
// HTML for the view function and script to store resources
{
let nonce_str = nonce_str.clone();
async move {
let resolvers = format!(
"<script{nonce_str}>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new \
Map();__LEPTOS_LOCAL_ONLY = {local_only};</script>"
);
if replace_blocks {
let mut blocks =
Vec::with_capacity(blocking_fragments.len());
while let Some((blocked_id, blocked_fragment)) =
blocking_fragments.next().await
{
blocks.push((blocked_id, blocked_fragment));
}
let prefix = prefix();
let mut shell = shell;
for (blocked_id, blocked_fragment) in blocks {
let open = format!("<!--suspense-open-{blocked_id}-->");
let close =
format!("<!--suspense-close-{blocked_id}-->");
let (first, rest) =
shell.split_once(&open).unwrap_or_default();
let (_fallback, rest) =
rest.split_once(&close).unwrap_or_default();
shell =
format!("{first}{blocked_fragment}{rest}").into();
}
format!("{prefix}{shell}{resolvers}")
} else {
let mut blocking = String::new();
let mut blocking_fragments = fragments_to_chunks(
nonce_str.clone(),
blocking_fragments,
);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
}
let prefix = prefix();
format!("{prefix}{shell}{resolvers}{blocking}")
}
}
},
)
.chain(ooo_body_stream_recurse(nonce_str, fragments, serializers));
(stream, runtime)
}
fn ooo_body_stream_recurse(
nonce_str: String,
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> Pin<Box<dyn Stream<Item = String>>> {
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
let fragments = fragments_to_chunks(nonce_str.clone(), fragments);
// stream data for each Resource as it resolves
let resources = render_serializers(nonce_str.clone(), serializers);
Box::pin(
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
fragments.chain(resources).chain(
futures::stream::once(async move {
let pending = SharedContext::pending_fragments();
if !pending.is_empty() {
let fragments = FuturesUnordered::new();
let serializers = SharedContext::serialization_resolvers();
for (fragment_id, data) in pending {
fragments.push(Box::pin(async move {
(fragment_id.clone(), data.out_of_order.await)
})
as Pin<Box<dyn Future<Output = (String, String)>>>);
}
Box::pin(ooo_body_stream_recurse(
nonce_str,
fragments,
serializers,
))
as Pin<Box<dyn Stream<Item = String>>>
} else {
Box::pin(futures::stream::once(async move {
Default::default()
}))
}
})
.flatten(),
),
)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
fn fragments_to_chunks(
nonce_str: String,
fragments: impl Stream<Item = (String, String)>,
) -> impl Stream<Item = String> {
fragments.map(move |(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script{nonce_str}>
(function() {{ let id = "{fragment_id}";
let open = undefined;
let close = undefined;
let walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
while(walker.nextNode()) {{
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
open = walker.currentNode;
}} else if(walker.currentNode.textContent == `suspense-close-${{id}}`) {{
close = walker.currentNode;
}}
}}
let range = new Range();
range.setStartAfter(open);
range.setEndBefore(close);
range.deleteContents();
let tpl = document.getElementById("{fragment_id}f");
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);}})()
</script>
"#
)
})
}
impl View {
/// Consumes the node and renders it into an HTML string.
///
/// This is __NOT__ the same as [`render_to_string`]. This
/// functions differs in that it assumes a runtime is in scope.
/// [`render_to_string`] creates, and disposes of a runtime for you.
///
/// # Panics
/// When called in a scope without a runtime. Use [`render_to_string`] instead.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn render_to_string(self) -> Oco<'static, str> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::logging::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently.\n",
);
self.render_to_string_helper(false)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn render_to_string_helper(
self,
dont_escape_text: bool,
) -> Oco<'static, str> {
match self {
View::Text(node) => {
if dont_escape_text {
node.content
} else {
html_escape::encode_safe(&node.content).to_string().into()
}
}
View::Component(node) => {
let content = || {
node.children
.into_iter()
.map(|node| {
node.render_to_string_helper(dont_escape_text)
})
.join("")
};
cfg_if! {
if #[cfg(debug_assertions)] {
let name = to_kebab_case(&node.name);
let content = format!(r#"{}{}{}"#,
node.id.to_marker(false, &name),
content(),
node.id.to_marker(true, &name),
);
if let Some(id) = node.view_marker {
format!("<!--leptos-view|{id}|open-->{content}<!--leptos-view|{id}|close-->").into()
} else {
content.into()
}
} else {
format!(
r#"{}{}"#,
content(),
node.id.to_marker(true)
).into()
}
}
}
View::Suspense(id, node) => format!(
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
View::CoreComponent(node)
.render_to_string_helper(dont_escape_text)
)
.into(),
View::CoreComponent(node) => {
let (id, name, wrap, content) = match node {
CoreComponent::Unit(u) => (
u.id,
"",
false,
Box::new(move || {
u.id.to_marker(
true,
#[cfg(debug_assertions)]
"unit",
)
})
as Box<dyn FnOnce() -> Oco<'static, str>>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
"dyn-child",
true,
Box::new(move || {
if let Some(child) = *child {
if let View::Text(t) = child {
// if we don't check if the string is empty,
// the HTML is an empty string; but an empty string
// is not a text node in HTML, so can't be updated
// in the future. so we put a one-space text node instead
let was_empty = t.content.is_empty();
let content = if was_empty {
" ".into()
} else {
t.content
};
// escape content unless we're in a <script> or <style>
let content = if dont_escape_text {
content
} else {
html_escape::encode_safe(&content)
.to_string()
.into()
};
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if !cfg!(debug_assertions) {
format!("<!>{content}",).into()
} else {
content
}
} else {
child.render_to_string_helper(
dont_escape_text,
)
}
} else {
"".into()
}
})
as Box<dyn FnOnce() -> Oco<'static, str>>,
)
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(move || {
children
.into_iter()
.flatten()
.map(|node| {
let id = node.id;
let is_el = matches!(
node.child,
View::Element(_)
);
let content = || {
node.child.render_to_string_helper(
dont_escape_text,
)
};
if is_el {
content()
} else {
format!(
"{}{}{}",
id.to_marker(
false,
#[cfg(debug_assertions)]
"each-item",
),
content(),
id.to_marker(
true,
#[cfg(debug_assertions)]
"each-item",
)
)
.into()
}
})
.join("")
.into()
})
as Box<dyn FnOnce() -> Oco<'static, str>>,
)
}
};
if wrap {
format!(
r#"{}{}{}"#,
id.to_marker(
false,
#[cfg(debug_assertions)]
name,
),
content(),
id.to_marker(
true,
#[cfg(debug_assertions)]
name,
),
)
.into()
} else {
content()
}
}
View::Element(el) => {
let is_script_or_style =
el.name == "script" || el.name == "style";
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(is_script_or_style),
})
.join("")
.into()
} else {
let tag_name: Oco<'_, str> = el.name;
let mut inner_html: Option<Oco<'_, str>> = None;
let attrs = el
.attrs
.into_iter()
.filter_map(
|(name, value)| -> Option<Oco<'static, str>> {
if value.is_empty() {
Some(format!(" {name}").into())
} else if name == "inner_html" {
inner_html = Some(value);
None
} else {
Some(
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into(),
)
}
},
)
.join("");
if el.is_void {
format!("<{tag_name}{attrs}/>").into()
} else if let Some(inner_html) = inner_html {
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
.into()
} else {
let children = match el.children {
ElementChildren::Empty => "".into(),
ElementChildren::Children(c) => c
.into_iter()
.map(|v| {
v.render_to_string_helper(
is_script_or_style,
)
})
.join("")
.into(),
ElementChildren::InnerHtml(h) => h,
// already handled this case above
ElementChildren::Chunks(_) => unreachable!(),
};
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
.into()
}
};
cfg_if! {
if #[cfg(debug_assertions)] {
if let Some(id) = el.view_marker {
format!("<!--leptos-view|{id}|open-->{el_html}<!--leptos-view|{id}|close-->").into()
} else {
el_html
}
} else {
el_html
}
}
}
View::Transparent(_) => Default::default(),
}
}
}
#[cfg(debug_assertions)]
pub(crate) fn to_kebab_case(name: &str) -> String {
if name.is_empty() {
return String::new();
}
let mut new_name = String::with_capacity(name.len() + 8);
let mut chars = name.chars();
new_name.push(
chars
.next()
.map(|mut c| {
if c.is_ascii() {
c.make_ascii_lowercase();
}
c
})
.unwrap(),
);
for mut char in chars {
if char.is_ascii_uppercase() {
char.make_ascii_lowercase();
new_name.push('-');
}
new_name.push(char);
}
new_name
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn render_serializers(
nonce_str: String,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> impl Stream<Item = String> {
serializers.map(move |(id, json)| {
let id = serde_json::to_string(&id).unwrap();
let json = json.replace('<', "\\u003c");
format!(
r#"<script{nonce_str}>
(function() {{ let val = {json:?};
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
}} }})();
</script>"#,
)
})
}
#[doc(hidden)]
pub fn escape_attr<T>(value: &T) -> Oco<'_, str>
where
T: AsRef<str>,
{
html_escape::encode_double_quoted_attribute(value).into()
}
pub(crate) trait ToMarker {
fn to_marker(
&self,
closing: bool,
#[cfg(debug_assertions)] component_name: &str,
) -> Oco<'static, str>;
}
impl ToMarker for HydrationKey {
#[inline(always)]
fn to_marker(
&self,
closing: bool,
#[cfg(debug_assertions)] mut component_name: &str,
) -> Oco<'static, str> {
#[cfg(debug_assertions)]
{
if component_name.is_empty() {
// NOTE:
// If the name is left empty, this will lead to invalid comments,
// so a placeholder is used here.
component_name = "<>";
}
if closing || component_name == "unit" {
format!("<!--hk={self}c|leptos-{component_name}-end-->").into()
} else {
format!("<!--hk={self}o|leptos-{component_name}-start-->")
.into()
}
}
#[cfg(not(debug_assertions))]
{
if closing {
format!("<!--hk={self}-->").into()
} else {
"".into()
}
}
}
}
impl ToMarker for Option<HydrationKey> {
#[inline(always)]
fn to_marker(
&self,
closing: bool,
#[cfg(debug_assertions)] component_name: &str,
) -> Oco<'static, str> {
self.map(|key| {
key.to_marker(
closing,
#[cfg(debug_assertions)]
component_name,
)
})
.unwrap_or("".into())
}
}

View File

@@ -1,531 +0,0 @@
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
//! Server-side HTML rendering utilities for in-order streaming and async rendering.
use crate::{
html::{ElementChildren, StringOrView},
ssr::{render_serializers, ToMarker},
CoreComponent, HydrationCtx, View,
};
use async_recursion::async_recursion;
use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt};
use itertools::Itertools;
use leptos_reactive::{
create_runtime, suspense::StreamChunk, Oco, RuntimeId, SharedContext,
};
use std::collections::VecDeque;
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
/// loaded in `<Suspense/>` elements have finished loading.
#[tracing::instrument(level = "trace", skip_all)]
pub async fn render_to_string_async(
view: impl FnOnce() -> View + 'static,
) -> String {
let mut buf = String::new();
let (stream, runtime) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
view,
|| "".into(),
|| {},
);
let mut stream = Box::pin(stream);
while let Some(chunk) = stream.next().await {
buf.push_str(&chunk);
}
runtime.dispose();
buf
}
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
/// in order:
/// 1. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 2. any serialized [Resource](leptos_reactive::Resource)s
#[tracing::instrument(level = "trace", skip_all)]
pub fn render_to_stream_in_order(
view: impl FnOnce() -> View + 'static,
) -> impl Stream<Item = String> {
render_to_stream_in_order_with_prefix(view, || "".into())
}
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
/// in order:
/// 1. `prefix`
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 3. any serialized [Resource](leptos_reactive::Resource)s
///
/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated
/// after the `view` is rendered, but before `<Suspense/>` nodes have resolved.
#[tracing::instrument(level = "trace", skip_all)]
pub fn render_to_stream_in_order_with_prefix(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
) -> impl Stream<Item = String> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::logging::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently.\n",
);
let (stream, runtime) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
view,
prefix,
|| {},
);
runtime.dispose();
stream
}
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
/// in order:
/// 1. `prefix`
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
/// 3. any serialized [Resource](leptos_reactive::Resource)s
///
/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated
/// after the `view` is rendered, but before `<Suspense/>` nodes have resolved.
#[tracing::instrument(level = "trace", skip_all)]
pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
view: impl FnOnce() -> View + 'static,
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
additional_context: impl FnOnce() + 'static,
) -> (impl Stream<Item = String>, RuntimeId) {
HydrationCtx::reset_id();
// create the runtime
let runtime = create_runtime();
// add additional context
additional_context();
// render view and return chunks
let view = view();
let blocking_fragments_ready = SharedContext::blocking_fragments_ready();
let chunks = view.into_stream_chunks();
let pending_resources =
serde_json::to_string(&SharedContext::pending_resources()).unwrap();
let (tx, rx) = futures::channel::mpsc::unbounded();
let (prefix_tx, prefix_rx) = futures::channel::oneshot::channel();
leptos_reactive::spawn_local(async move {
blocking_fragments_ready.await;
let remaining_chunks = handle_blocking_chunks(tx.clone(), chunks).await;
let prefix = prefix();
prefix_tx.send(prefix).expect("to send prefix");
handle_chunks(tx, remaining_chunks).await;
});
let nonce = crate::nonce::use_nonce();
let nonce_str = nonce
.as_ref()
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
let local_only = SharedContext::fragments_with_local_resources();
let local_only = serde_json::to_string(&local_only).unwrap();
let stream = futures::stream::once({
let nonce_str = nonce_str.clone();
async move {
let prefix = prefix_rx.await.expect("to receive prefix");
format!(
r#"
{prefix}
<script{nonce_str}>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
__LEPTOS_LOCAL_ONLY = {local_only};
</script>
"#
)
}
})
.chain(rx)
.chain(
futures::stream::once(async move {
let serializers = SharedContext::serialization_resolvers();
render_serializers(nonce_str, serializers)
})
.flatten(),
);
(stream, runtime)
}
#[tracing::instrument(level = "trace", skip_all)]
#[async_recursion(?Send)]
async fn handle_blocking_chunks(
tx: UnboundedSender<String>,
mut queued_chunks: VecDeque<StreamChunk>,
) -> VecDeque<StreamChunk> {
let mut buffer = String::new();
while let Some(chunk) = queued_chunks.pop_front() {
match chunk {
StreamChunk::Sync(sync) => buffer.push_str(&sync),
StreamChunk::Async {
chunks,
should_block,
} => {
if should_block {
// add static HTML before the Suspense and stream it down
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send async HTML chunk");
// send the inner stream
let suspended = chunks.await;
handle_blocking_chunks(tx.clone(), suspended).await;
} else {
// TODO: should probably first check if there are any *other* blocking chunks
queued_chunks.push_front(StreamChunk::Async {
chunks,
should_block: false,
});
break;
}
}
}
}
// send final sync chunk
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send final HTML chunk");
queued_chunks
}
#[tracing::instrument(level = "trace", skip_all)]
#[async_recursion(?Send)]
async fn handle_chunks(
tx: UnboundedSender<String>,
chunks: VecDeque<StreamChunk>,
) {
let mut buffer = String::new();
for chunk in chunks {
match chunk {
StreamChunk::Sync(sync) => buffer.push_str(&sync),
StreamChunk::Async { chunks, .. } => {
// add static HTML before the Suspense and stream it down
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send async HTML chunk");
// send the inner stream
let suspended = chunks.await;
handle_chunks(tx.clone(), suspended).await;
}
}
}
// send final sync chunk
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send final HTML chunk");
}
impl View {
/// Renders the view into a set of HTML chunks that can be streamed.
#[tracing::instrument(level = "trace", skip_all)]
pub fn into_stream_chunks(self) -> VecDeque<StreamChunk> {
let mut chunks = VecDeque::new();
self.into_stream_chunks_helper(&mut chunks, false);
chunks
}
#[tracing::instrument(level = "trace", skip_all)]
fn into_stream_chunks_helper(
self,
chunks: &mut VecDeque<StreamChunk>,
dont_escape_text: bool,
) {
match self {
View::Suspense(id, view) => {
let id = id.to_string();
if let Some(data) = SharedContext::take_pending_fragment(&id) {
chunks.push_back(StreamChunk::Async {
chunks: data.in_order,
should_block: data.should_block,
});
} else {
// if not registered, means it was already resolved
View::CoreComponent(view)
.into_stream_chunks_helper(chunks, dont_escape_text);
}
}
View::Text(node) => {
chunks.push_back(StreamChunk::Sync(node.content))
}
View::Component(node) => {
#[cfg(debug_assertions)]
let name = crate::ssr::to_kebab_case(&node.name);
if cfg!(debug_assertions) {
chunks.push_back(StreamChunk::Sync(node.id.to_marker(
false,
#[cfg(debug_assertions)]
&name,
)));
}
for child in node.children {
child.into_stream_chunks_helper(chunks, dont_escape_text);
}
chunks.push_back(StreamChunk::Sync(node.id.to_marker(
true,
#[cfg(debug_assertions)]
&name,
)));
}
View::Element(el) => {
let is_script_or_style =
el.name == "script" || el.name == "style";
#[cfg(debug_assertions)]
if let Some(id) = &el.view_marker {
chunks.push_back(StreamChunk::Sync(
format!("<!--leptos-view|{id}|open-->").into(),
));
}
if let ElementChildren::Chunks(el_chunks) = el.children {
for chunk in el_chunks {
match chunk {
StringOrView::String(string) => {
chunks.push_back(StreamChunk::Sync(string))
}
StringOrView::View(view) => view()
.into_stream_chunks_helper(
chunks,
is_script_or_style,
),
}
}
} else {
let tag_name = el.name;
let mut inner_html = None;
let attrs = el
.attrs
.into_iter()
.filter_map(
|(name, value)| -> Option<Oco<'static, str>> {
if value.is_empty() {
Some(format!(" {name}").into())
} else if name == "inner_html" {
inner_html = Some(value);
None
} else {
Some(
format!(
" {name}=\"{}\"",
html_escape::encode_double_quoted_attribute(&value)
)
.into(),
)
}
},
)
.join("");
if el.is_void {
chunks.push_back(StreamChunk::Sync(
format!("<{tag_name}{attrs}/>").into(),
));
} else if let Some(inner_html) = inner_html {
chunks.push_back(StreamChunk::Sync(
format!(
"<{tag_name}{attrs}>{inner_html}</{tag_name}>"
)
.into(),
));
} else {
chunks.push_back(StreamChunk::Sync(
format!("<{tag_name}{attrs}>").into(),
));
match el.children {
ElementChildren::Empty => {}
ElementChildren::Children(children) => {
for child in children {
child.into_stream_chunks_helper(
chunks,
is_script_or_style,
);
}
}
ElementChildren::InnerHtml(inner_html) => {
chunks.push_back(StreamChunk::Sync(inner_html))
}
// handled above
ElementChildren::Chunks(_) => unreachable!(),
}
chunks.push_back(StreamChunk::Sync(
format!("</{tag_name}>").into(),
));
}
}
#[cfg(debug_assertions)]
if let Some(id) = &el.view_marker {
chunks.push_back(StreamChunk::Sync(
format!("<!--leptos-view|{id}|close-->").into(),
));
}
}
View::Transparent(_) => {}
View::CoreComponent(node) => {
let (id, name, wrap, content) = match node {
CoreComponent::Unit(u) => (
u.id,
"",
false,
Box::new(move |chunks: &mut VecDeque<StreamChunk>| {
chunks.push_back(StreamChunk::Sync(
u.id.to_marker(
true,
#[cfg(debug_assertions)]
"unit",
),
));
})
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
),
CoreComponent::DynChild(node) => {
let child = node.child.take();
(
node.id,
"dyn-child",
true,
Box::new(
move |chunks: &mut VecDeque<StreamChunk>| {
if let Some(child) = *child {
if let View::Text(t) = child {
// if we don't check if the string is empty,
// the HTML is an empty string; but an empty string
// is not a text node in HTML, so can't be updated
// in the future. so we put a one-space text node instead
let was_empty =
t.content.is_empty();
let content = if was_empty {
" ".into()
} else {
t.content
};
// escape content unless we're in a <script> or <style>
let content = if dont_escape_text {
content
} else {
html_escape::encode_safe(
&content,
)
.to_string()
.into()
};
// On debug builds, `DynChild` has two marker nodes,
// so there is no way for the text to be merged with
// surrounding text when the browser parses the HTML,
// but in release, `DynChild` only has a trailing marker,
// and the browser automatically merges the dynamic text
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
chunks.push_back(
if !cfg!(debug_assertions) {
StreamChunk::Sync(
format!(
"<!>{}",
html_escape::encode_safe(
&content
)
)
.into(),
)
} else {
StreamChunk::Sync(html_escape::encode_safe(
&content
).to_string().into())
},
);
} else {
child.into_stream_chunks_helper(
chunks,
dont_escape_text,
);
}
}
},
)
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
)
}
CoreComponent::Each(node) => {
let children = node.children.take();
(
node.id,
"each",
true,
Box::new(
move |chunks: &mut VecDeque<StreamChunk>| {
for node in children.into_iter().flatten() {
let id = node.id;
let is_el = matches!(
node.child,
View::Element(_)
);
#[cfg(debug_assertions)]
if !is_el {
chunks.push_back(StreamChunk::Sync(
id.to_marker(
false,
"each-item",
),
))
};
node.child.into_stream_chunks_helper(
chunks,
dont_escape_text,
);
if !is_el {
chunks.push_back(
StreamChunk::Sync(
id.to_marker(
true,
#[cfg(
debug_assertions
)]
"each-item",
),
),
);
}
}
},
)
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
)
}
};
if wrap {
#[cfg(debug_assertions)]
{
chunks.push_back(StreamChunk::Sync(
id.to_marker(false, name),
));
}
content(chunks);
chunks.push_back(StreamChunk::Sync(id.to_marker(
true,
#[cfg(debug_assertions)]
name,
)));
} else {
content(chunks);
}
}
}
}
}

View File

@@ -1,302 +0,0 @@
//! Exports types for working with SVG elements.
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey};
use super::{AnyElement, ElementDescriptor, HtmlElement};
use crate::HydrationCtx;
use leptos_reactive::Oco;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use once_cell::unsync::Lazy as LazyCell;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::JsCast;
macro_rules! generate_svg_tags {
(
$(
#[$meta:meta]
$(#[$void:ident])?
$tag:ident $(- $second:ident $(- $third:ident)?)? $(@ $trailing_:pat)?
),* $(,)?
) => {
paste::paste! {
$(
#[cfg(all(target_arch = "wasm32", feature = "web"))]
thread_local! {
static [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]: LazyCell<web_sys::HtmlElement> = LazyCell::new(|| {
crate::document()
.create_element_ns(
Some(wasm_bindgen::intern("http://www.w3.org/2000/svg")),
concat![
stringify!($tag),
$(
"-", stringify!($second),
$(
"-", stringify!($third)
)?
)?
],
)
.unwrap()
.unchecked_into()
});
}
#[derive(Clone, Debug)]
#[$meta]
pub struct [<$tag:camel $($second:camel $($third:camel)?)?>] {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element: web_sys::HtmlElement,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id: Option<HydrationKey>,
}
impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn default() -> Self {
#[allow(unused)]
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
let element = if HydrationCtx::is_hydrating() && id.is_some() {
let id = id.unwrap();
if let Some(el) = crate::hydration::get_element(&id.to_string()) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
);
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
} else {
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
};
#[cfg(all(target_arch = "wasm32", feature = "web", not(feature = "hydrate")))]
let element = [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
);
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
element,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id
}
}
}
impl std::ops::Deref for [<$tag:camel $($second:camel $($third:camel)?)?>] {
type Target = web_sys::SvgElement;
fn deref(&self) -> &Self::Target {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
use wasm_bindgen::JsCast;
return &self.element.unchecked_ref();
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}");
}
}
impl std::convert::AsRef<web_sys::HtmlElement> for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn as_ref(&self) -> &web_sys::HtmlElement {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return &self.element;
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}");
}
}
impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] {
fn name(&self) -> Oco<'static, str> {
stringify!($tag).into()
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> &Option<HydrationKey> {
&self.id
}
generate_svg_tags! { @void $($void)? }
}
impl From<HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>> for HtmlElement<AnyElement> {
fn from(element: HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>) -> Self {
element.into_any()
}
}
#[$meta]
#[allow(non_snake_case)]
pub fn [<$tag $(_ $second $(_ $third)?)? $($trailing_)?>]() -> HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]> {
HtmlElement::new([<$tag:camel $($second:camel $($third:camel)?)?>]::default())
}
)*
}
};
(@void) => {};
(@void void) => {
fn is_void(&self) -> bool {
true
}
};
}
generate_svg_tags![
/// SVG Element.
a,
/// SVG Element.
animate,
/// SVG Element.
animateMotion,
/// SVG Element.
animateTransform,
/// SVG Element.
circle,
/// SVG Element.
clipPath,
/// SVG Element.
defs,
/// SVG Element.
desc,
/// SVG Element.
discard,
/// SVG Element.
ellipse,
/// SVG Element.
feBlend,
/// SVG Element.
feColorMatrix,
/// SVG Element.
feComponentTransfer,
/// SVG Element.
feComposite,
/// SVG Element.
feConvolveMatrix,
/// SVG Element.
feDiffuseLighting,
/// SVG Element.
feDisplacementMap,
/// SVG Element.
feDistantLight,
/// SVG Element.
feDropShadow,
/// SVG Element.
feFlood,
/// SVG Element.
feFuncA,
/// SVG Element.
feFuncB,
/// SVG Element.
feFuncG,
/// SVG Element.
feFuncR,
/// SVG Element.
feGaussianBlur,
/// SVG Element.
feImage,
/// SVG Element.
feMerge,
/// SVG Element.
feMergeNode,
/// SVG Element.
feMorphology,
/// SVG Element.
feOffset,
/// SVG Element.
fePointLight,
/// SVG Element.
feSpecularLighting,
/// SVG Element.
feSpotLight,
/// SVG Element.
feTile,
/// SVG Element.
feTurbulence,
/// SVG Element.
filter,
/// SVG Element.
foreignObject,
/// SVG Element.
g,
/// SVG Element.
hatch,
/// SVG Element.
hatchpath,
/// SVG Element.
image,
/// SVG Element.
line,
/// SVG Element.
linearGradient,
/// SVG Element.
marker,
/// SVG Element.
mask,
/// SVG Element.
metadata,
/// SVG Element.
mpath,
/// SVG Element.
path,
/// SVG Element.
pattern,
/// SVG Element.
polygon,
/// SVG Element.
polyline,
/// SVG Element.
radialGradient,
/// SVG Element.
rect,
/// SVG Element.
script,
/// SVG Element.
set,
/// SVG Element.
stop,
/// SVG Element.
style,
/// SVG Element.
svg,
/// SVG Element.
switch,
/// SVG Element.
symbol,
/// SVG Element.
text,
/// SVG Element.
textPath,
/// SVG Element.
title,
/// SVG Element.
tspan,
/// SVG Element.
use @_,
/// SVG Element.
view,
];

View File

@@ -1,49 +0,0 @@
use crate::{IntoView, View};
use std::{any::Any, fmt, rc::Rc};
/// Wrapper for arbitrary data that can be passed through the view.
#[derive(Clone)]
#[repr(transparent)]
pub struct Transparent(Rc<dyn Any>);
impl Transparent {
/// Creates a new wrapper for this data.
#[inline(always)]
pub fn new<T>(value: T) -> Self
where
T: 'static,
{
Self(Rc::new(value))
}
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
#[inline(always)]
pub fn downcast_ref<T>(&self) -> Option<&T>
where
T: 'static,
{
self.0.downcast_ref()
}
}
impl fmt::Debug for Transparent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Transparent").finish()
}
}
impl PartialEq for Transparent {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(&self.0, &other.0)
}
}
impl Eq for Transparent {}
impl IntoView for Transparent {
#[inline(always)]
fn into_view(self) -> View {
View::Transparent(self)
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.7.0-alpha2"
version = "0.7.0-beta"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -34,13 +34,14 @@ log = "0.4"
typed-builder = "0.18"
trybuild = "1"
leptos = { path = "../leptos" }
server_fn = { path = "../server_fn", features = ["cbor"] }
insta = "1.29"
serde = "1"
[features]
csr = []
hydrate = []
ssr = ["server_fn_macro/ssr"]
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = []
experimental-islands = []

View File

@@ -11,13 +11,13 @@ dependencies = [
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly-2024-04-14", "test", "--doc"]
args = ["+nightly-2024-08-01", "test", "--doc"]
cwd = "example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly-2024-04-14", "doc"]
args = ["+nightly-2024-08-01", "doc"]
cwd = "example"
install_crate = false

View File

@@ -1,4 +1,4 @@
use leptos::*;
use leptos::prelude::*;
#[component]
pub fn TestComponent(
@@ -38,7 +38,7 @@ pub fn TestComponent(
}
#[component]
fn TestMutCallback<F>(mut callback: F, value: &'static str) -> impl IntoView
pub fn TestMutCallback<F>(mut callback: F, value: &'static str) -> impl IntoView
where
F: FnMut(u32) + 'static,
{
@@ -46,9 +46,7 @@ where
view! {
<button on:click=move |_| {
callback(5);
}>
{value}
</button>
}>{value}</button>
<TestComponent key="test"/>
}
}

View File

@@ -227,7 +227,7 @@ impl ToTokens for Model {
};
let island_serialized_props = if is_island_with_other_props {
quote! {
.attr("data-props", _leptos_ser_props)
.with_props( _leptos_ser_props)
}
} else {
quote! {}
@@ -260,11 +260,21 @@ impl ToTokens for Model {
let component = if *is_island {
quote! {
{
::leptos::tachys::html::islands::Island::new(
#component_id,
#component
)
#island_serialized_props
if ::leptos::reactive_graph::owner::Owner::current_shared_context()
.map(|sc| sc.get_is_hydrating())
.unwrap_or(false) {
::leptos::either::Either::Left(
#component
)
} else {
::leptos::either::Either::Right(
::leptos::tachys::html::islands::Island::new(
#component_id,
#component
)
#island_serialized_props
)
}
}
}
} else {
@@ -285,7 +295,15 @@ impl ToTokens for Model {
let wrapped_children = if is_island_with_children {
quote! {
use leptos::tachys::view::any_view::IntoAny;
let children = Box::new(|| ::leptos::tachys::html::islands::IslandChildren::new(children()).into_any());
let children = Box::new(|| {
let sc = ::leptos::reactive_graph::owner::Owner::current_shared_context().unwrap();
let prev = sc.get_is_hydrating();
let value = ::leptos::reactive_graph::owner::Owner::with_no_hydration(||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
);
sc.set_is_hydrating(prev);
value
});
}
} else {
quote! {}
@@ -375,16 +393,15 @@ impl ToTokens for Model {
} else {
quote! {}
};
let deserialize_island_props = quote! {}; /*if is_island_with_other_props {
quote! {
let props = el.dataset().get("props") // TODO ::leptos::wasm_bindgen::intern("props"))
.and_then(|data| ::leptos::serde_json::from_str::<#props_serialized_name>(&data).ok())
.expect("could not deserialize props");
}
} else {
quote! {}
};*/
// TODO
let deserialize_island_props = if is_island_with_other_props {
quote! {
let props = el.dataset().get(::leptos::wasm_bindgen::intern("props"))
.and_then(|data| ::leptos::serde_json::from_str::<#props_serialized_name>(&data).ok())
.expect("could not deserialize props");
}
} else {
quote! {}
};
quote! {
#[::leptos::wasm_bindgen::prelude::wasm_bindgen(wasm_bindgen = ::leptos::wasm_bindgen)]
@@ -608,8 +625,6 @@ impl Docs {
let mut quote_ws = "".to_string();
let mut view_code_fence_state = ViewCodeFenceState::Outside;
// todo fix docs stuff
const RUST_START: &str = "# let runtime = ::leptos::create_runtime();";
const RUST_END: &str = "# runtime.dispose();";
const RSX_START: &str = "# ::leptos::view! {";
const RSX_END: &str = "# };";
@@ -637,15 +652,12 @@ impl Docs {
.trim_start();
vec![
format!("{leading_ws}{quotes}{rust_options}"),
format!("{leading_ws}{RUST_START}"),
format!("{leading_ws}"),
]
}
ViewCodeFenceState::Rust if trimmed_doc == quotes => {
view_code_fence_state = ViewCodeFenceState::Outside;
vec![
format!("{leading_ws}{RUST_END}"),
doc.to_owned(),
]
vec![format!("{leading_ws}"), doc.to_owned()]
}
ViewCodeFenceState::Rust
if trimmed_doc.starts_with('<') =>
@@ -694,7 +706,7 @@ impl Docs {
if view_code_fence_state != ViewCodeFenceState::Outside {
if view_code_fence_state == ViewCodeFenceState::Rust {
attrs.push((format!("{quote_ws}{RUST_END}"), Span::call_site()))
attrs.push((quote_ws.clone(), Span::call_site()))
} else {
attrs.push((format!("{quote_ws}{RSX_END}"), Span::call_site()))
}

View File

@@ -0,0 +1,166 @@
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse::Parse, parse_macro_input, ImplItem, ItemImpl};
pub fn custom_view_impl(tokens: TokenStream) -> TokenStream {
let input = parse_macro_input!(tokens as CustomViewMacroInput);
input.into_token_stream().into()
}
#[derive(Debug)]
struct CustomViewMacroInput {
impl_block: ItemImpl,
}
impl Parse for CustomViewMacroInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let impl_block = input.parse()?;
Ok(CustomViewMacroInput { impl_block })
}
}
impl ToTokens for CustomViewMacroInput {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let ItemImpl {
impl_token,
generics,
self_ty,
items,
..
} = &self.impl_block;
let impl_span = &impl_token;
let view_ty = items
.iter()
.find_map(|item| match item {
ImplItem::Type(ty) => (ty.ident == "View").then_some(&ty.ty),
_ => None,
})
.unwrap_or_else(|| {
proc_macro_error::abort!(
impl_span,
"You must include `type View = ...;` to specify the type. \
In most cases, this will be `type View = AnyView<Rndr>;"
)
});
let view_fn = items
.iter()
.find_map(|item| match item {
ImplItem::Fn(f) => {
(f.sig.ident == "into_view").then_some(&f.block)
}
_ => None,
})
.unwrap_or_else(|| {
proc_macro_error::abort!(
impl_span,
"You must include `fn into_view(self) -> Self::View` to \
specify the view function."
)
});
let generic_params = &generics.params;
let where_preds =
&generics.where_clause.as_ref().map(|wc| &wc.predicates);
tokens.extend(quote! {
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::Render<Rndr> for #self_ty
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
type State = <#view_ty as ::leptos::tachys::view::Render<Rndr>>::State;
fn build(self) -> Self::State {
let view = #view_fn;
view.build()
}
fn rebuild(self, state: &mut Self::State) {
let view = #view_fn;
view.rebuild(state);
}
}
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::add_attr::AddAnyAttr<Rndr> for #self_ty
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
type Output<SomeNewAttr: ::leptos::tachys::html::attribute::Attribute<Rndr>> =
<#view_ty as ::leptos::tachys::view::add_attr::AddAnyAttr<Rndr>>::Output<SomeNewAttr>;
fn add_any_attr<NewAttr: ::leptos::tachys::html::attribute::Attribute<Rndr>>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: ::leptos::tachys::view::RenderHtml<Rndr>,
{
let view = #view_fn;
view.add_any_attr(attr)
}
}
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::RenderHtml<Rndr> for #self_ty
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
type AsyncOutput = <#view_ty as ::leptos::tachys::view::RenderHtml<Rndr>>::AsyncOutput;
const MIN_LENGTH: usize = <#view_ty as ::leptos::tachys::view::RenderHtml<Rndr>>::MIN_LENGTH;
async fn resolve(self) -> Self::AsyncOutput {
let view = #view_fn;
::leptos::tachys::view::RenderHtml::<Rndr>::resolve(view).await
}
fn dry_resolve(&mut self) {
// TODO... The problem is that view_fn expects to take self
// dry_resolve is the only one that takes &mut self
// this can only have an effect if walking over the view would read from
// resources that are not otherwise read synchronously, which is an interesting
// edge case to handle but probably (?) irrelevant for most actual use cases of
// this macro
}
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut ::leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
) {
let view = #view_fn;
::leptos::tachys::view::RenderHtml::<Rndr>::to_html_with_buf(
view,
buf,
position,
escape,
mark_branches
);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut ::leptos::tachys::ssr::StreamBuilder,
position: &mut ::leptos::tachys::view::Position,
escape: bool,
mark_branches: bool,
) where
Self: Sized,
{
let view = #view_fn;
::leptos::tachys::view::RenderHtml::<Rndr>::to_html_async_with_buf::<OUT_OF_ORDER>(
view,
buf,
position,
escape,
mark_branches
);
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &::leptos::tachys::hydration::Cursor<Rndr>,
position: &::leptos::tachys::view::PositionState,
) -> Self::State {
let view = #view_fn;
::leptos::tachys::view::RenderHtml::<Rndr>::hydrate::<FROM_SERVER>(
view, cursor, position
)
}
}
});
}
}

View File

@@ -19,6 +19,7 @@ mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
mod component;
mod custom_view;
mod slice;
mod slot;
@@ -27,48 +28,41 @@ mod slot;
///
/// 1. Text content should be provided as a Rust string, i.e., double-quoted:
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// view! { <p>"Heres some text"</p> };
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// view! { <p>"Heres some text"</p> }
/// # }
/// # runtime.dispose();
/// ```
///
/// 2. Self-closing tags need an explicit `/` as in XML/XHTML
/// ```rust,compile_fail
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
///
/// # fn test() -> impl IntoView {
/// // ❌ not like this
/// view! { <input type="text" name="name"> }
/// # ;
/// # }
/// # runtime.dispose();
/// ```
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// // ✅ add that slash
/// view! { <input type="text" name="name" /> }
/// # ;
/// # }
/// # runtime.dispose();
/// ```
///
/// 3. Components (functions annotated with `#[component]`) can be inserted as camel-cased tags. (Generics
/// on components are specified as `<Component<T>/>`, not the turbofish `<Component::<T>/>`.)
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # use leptos::prelude::*;
///
/// # #[component]
/// # fn Counter(initial_value: i32) -> impl IntoView { view! { <p></p>} }
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # fn test() -> impl IntoView {
/// view! { <div><Counter initial_value=3 /></div> }
/// # ;
/// # }
/// # runtime.dispose();
/// ```
///
/// 4. Dynamic content can be wrapped in curly braces (`{ }`) to insert text nodes, elements, or set attributes.
@@ -80,9 +74,9 @@ mod slot;
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
///
/// ```rust,ignore
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
///
/// # fn test() -> impl IntoView {
/// let (count, set_count) = create_signal(0);
///
/// view! {
@@ -95,15 +89,13 @@ mod slot;
/// }
/// # ;
/// # };
/// # runtime.dispose();
/// ```
///
/// 5. Event handlers can be added with `on:` attributes. In most cases, the events are given the correct type
/// based on the event name.
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// view! {
/// <button on:click=|ev| {
/// log::debug!("click event: {ev:#?}");
@@ -111,18 +103,15 @@ mod slot;
/// "Click me"
/// </button>
/// }
/// # ;
/// # };
/// # runtime.dispose();
/// # }
/// ```
///
/// 6. DOM properties can be set with `prop:` attributes, which take any primitive type or `JsValue` (or a signal
/// that returns a primitive or JsValue). They can also take an `Option`, in which case `Some` sets the property
/// and `None` deletes the property.
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// let (name, set_name) = create_signal("Alice".to_string());
///
/// view! {
@@ -134,53 +123,41 @@ mod slot;
/// on:click=move |ev| set_name.set(event_target_value(&ev)) // `event_target_value` is a useful little Leptos helper
/// />
/// }
/// # ;
/// # };
/// # runtime.dispose();
/// # }
/// ```
///
/// 7. Classes can be toggled with `class:` attributes, which take a `bool` (or a signal that returns a `bool`).
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// let (count, set_count) = create_signal(2);
/// view! { <div class:hidden-div={move || count.get() < 3}>"Now you see me, now you dont."</div> }
/// # ;
/// # }
/// # runtime.dispose();
/// ```
///
/// Class names can include dashes, and since v0.5.0 can include a dash-separated segment of only numbers.
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// let (count, set_count) = create_signal(2);
/// view! { <div class:hidden-div-25={move || count.get() < 3}>"Now you see me, now you dont."</div> }
/// # ;
/// # };
/// # runtime.dispose();
/// # }
/// ```
///
/// Class names cannot include special symbols.
/// ```rust,compile_fail
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// let (count, set_count) = create_signal(2);
/// // class:hidden-[div]-25 is invalid attribute name
/// view! { <div class:hidden-[div]-25={move || count.get() < 3}>"Now you see me, now you dont."</div> }
/// # ;
/// # };
/// # runtime.dispose();
/// # }
/// ```
///
/// However, you can pass arbitrary class names using the syntax `class=("name", value)`.
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// let (count, set_count) = create_signal(2);
/// // this allows you to use CSS frameworks that include complex class names
/// view! {
@@ -190,16 +167,14 @@ mod slot;
/// "Now you see me, now you dont."
/// </div>
/// }
/// # ;
/// # };
/// # runtime.dispose();
/// # }
/// ```
///
/// 8. Individual styles can also be set with `style:` or `style=("property-name", value)` syntax.
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
///
/// # fn test() -> impl IntoView {
/// let (x, set_x) = create_signal(0);
/// let (y, set_y) = create_signal(0);
/// view! {
@@ -212,67 +187,57 @@ mod slot;
/// "Moves when coordinates change"
/// </div>
/// }
/// # ;
/// # };
/// # runtime.dispose();
/// # }
/// ```
///
/// 9. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
///
/// # fn test() -> impl IntoView {
/// use leptos::html::Input;
///
/// let (value, set_value) = create_signal(0);
/// let my_input = create_node_ref::<Input>();
/// view! { <input type="text" _ref=my_input/> }
/// let (value, set_value) = signal(0);
/// let my_input = NodeRef::<Input>::new();
/// view! { <input type="text" node_ref=my_input/> }
/// // `my_input` now contains an `Element` that we can use anywhere
/// # ;
/// # };
/// # runtime.dispose();
/// ```
///
/// 10. You can add the same class to every element in the view by passing in a special
/// `class = {/* ... */},` argument after ``. This is useful for injecting a class
/// provided by a scoped styling library.
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
///
/// # fn test() -> impl IntoView {
/// let class = "mycustomclass";
/// view! { class = class,
/// <div> // will have class="mycustomclass"
/// <p>"Some text"</p> // will also have class "mycustomclass"
/// </div>
/// }
/// # ;
/// # };
/// # runtime.dispose();
/// # }
/// ```
///
/// 11. You can set any HTML elements `innerHTML` with the `inner_html` attribute on an
/// element. Be careful: this HTML will not be escaped, so you should ensure that it
/// only contains trusted input.
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// # fn test() -> impl IntoView {
/// let html = "<p>This HTML will be injected.</p>";
/// view! {
/// <div inner_html=html/>
/// }
/// # ;
/// # };
/// # runtime.dispose();
/// # }
/// ```
///
/// Heres a simple example that shows off several of these features, put together
/// ```rust
/// # use leptos::*;
///
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// # use leptos::prelude::*;
/// pub fn SimpleCounter() -> impl IntoView {
/// // create a reactive signal with the initial value
/// let (value, set_value) = create_signal(0);
@@ -292,8 +257,6 @@ mod slot;
/// </div>
/// }
/// }
/// # ;
/// # }
/// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro]
@@ -364,7 +327,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
///
/// Heres how you would define and use a simple Leptos component which can accept custom properties for a name and age:
/// ```rust
/// # use leptos::*;
/// # use leptos::prelude::*;
/// use std::time::Duration;
///
/// #[component]
@@ -411,7 +374,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// a particular tag is a component, not an HTML element.
///
/// ```
/// # use leptos::*;
/// # use leptos::prelude::*;
///
/// // PascalCase: Generated component will be called MyComponent
/// #[component]
@@ -422,48 +385,15 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// fn my_snake_case_component() -> impl IntoView {}
/// ```
///
/// * You can pass generic arguments, and they can either be defined in a `where` clause
/// or inline in the generic block, but not in an `impl` in function argument position.
///
/// ```compile_error
/// // ❌ This won't work.
/// # use leptos::*;
/// use leptos::html::Div;
///
/// #[component]
/// fn MyComponent(render_prop: impl Fn() -> HtmlElement<Div>) -> impl IntoView {
/// }
/// ```
///
/// ```
/// // ✅ Do this instead
/// # use leptos::*;
/// use leptos::html::Div;
///
/// #[component]
/// fn MyComponent<T>(render_prop: T) -> impl IntoView
/// where
/// T: Fn() -> HtmlElement<Div>,
/// {
/// }
///
/// // or
/// #[component]
/// fn MyComponent2<T: Fn() -> HtmlElement<Div>>(
/// render_prop: T,
/// ) -> impl IntoView {
/// }
/// ```
///
/// 5. You can access the children passed into the component with the `children` property, which takes
/// an argument of the type `Children`. This is an alias for `Box<dyn FnOnce() -> Fragment>`.
/// an argument of the type `Children`. This is an alias for `Box<dyn FnOnce() -> AnyView<_>>`.
/// If you need `children` to be a `Fn` or `FnMut`, you can use the `ChildrenFn` or `ChildrenFnMut`
/// type aliases.
/// type aliases. If you want to iterate over the children, you can take `ChildrenFragment`.
///
/// ```
/// # use leptos::*;
/// # use leptos::prelude::*;
/// #[component]
/// fn ComponentWithChildren(children: Children) -> impl IntoView {
/// fn ComponentWithChildren(children: ChildrenFragment) -> impl IntoView {
/// view! {
/// <ul>
/// {children()
@@ -502,7 +432,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
/// specified as either `None` or `Some(T)`.
/// ```rust
/// # use leptos::*;
/// # use leptos::prelude::*;
///
/// #[component]
/// pub fn MyComponent(
@@ -570,7 +500,7 @@ pub fn component(
///
/// ## Example
/// ```rust,ignore
/// use leptos::*;
/// use leptos::prelude::*;
///
/// #[component]
/// pub fn App() -> impl IntoView {
@@ -656,7 +586,7 @@ fn component_macro(s: TokenStream, island: bool) -> TokenStream {
///
/// Heres how you would define and use a simple Leptos component which can accept a custom slot:
/// ```rust
/// # use leptos::*;
/// # use leptos::prelude::*;
/// use std::time::Duration;
///
/// #[slot]
@@ -668,16 +598,10 @@ fn component_macro(s: TokenStream, island: bool) -> TokenStream {
///
/// #[component]
/// fn HelloComponent(
///
/// /// Component slot, should be passed through the <HelloSlot slot> syntax.
/// hello_slot: HelloSlot,
/// ) -> impl IntoView {
/// // mirror the children from the slot, if any were passed
/// if let Some(children) = hello_slot.children {
/// (children)().into_view()
/// } else {
/// ().into_view()
/// }
/// hello_slot.children.map(|children| children())
/// }
///
/// #[component]
@@ -702,7 +626,7 @@ fn component_macro(s: TokenStream, island: bool) -> TokenStream {
///
/// ```compile_error
/// // ❌ This won't work
/// # use leptos::*;
/// # use leptos::prelude::*;
///
/// #[slot]
/// struct SlotWithChildren {
@@ -728,7 +652,7 @@ fn component_macro(s: TokenStream, island: bool) -> TokenStream {
///
/// ```
/// // ✅ Do this instead
/// # use leptos::*;
/// # use leptos::prelude::*;
///
/// #[slot]
/// struct SlotWithChildren {
@@ -908,9 +832,8 @@ pub fn params_derive(
/// Can be used to access deeply nested fields within a global state object.
///
/// ```rust
/// # use leptos::{create_runtime, create_rw_signal};
/// # use leptos::prelude::*;
/// # use leptos_macro::slice;
/// # let runtime = create_runtime();
///
/// #[derive(Default)]
/// pub struct Outer {
@@ -924,7 +847,7 @@ pub fn params_derive(
/// inner_name: String,
/// }
///
/// let outer_signal = create_rw_signal(Outer::default());
/// let outer_signal = RwSignal::new(Outer::default());
///
/// let (count, set_count) = slice!(outer_signal.count);
///
@@ -935,3 +858,36 @@ pub fn params_derive(
pub fn slice(input: TokenStream) -> TokenStream {
slice::slice_impl(input)
}
/// Implements the traits needed to make something [`IntoView`] on some type.
///
/// The renderer relies on the implementation of several traits, implementing which for a custom
/// struct involves significant boilerplate. This macro is intended to make it easier to implement
/// a view type for custom data, by allowing you to provide some custom view logic for the type.
///
/// ```rust
/// use leptos::custom_view;
///
/// struct Foo<T>(T);
///
/// #[custom_view]
/// impl<T> CustomView for Foo<T>
/// where
/// T: ToString + Send + 'static,
/// {
/// // this will usually be `AnyView<Rndr>`, but for simple types
/// // you may be able to specify the output type easily
/// type View = String;
///
/// fn into_view(self) -> Self::View {
/// self.0.to_string().to_ascii_uppercase()
/// }
/// }
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute]
pub fn custom_view(
_args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
custom_view::custom_view_impl(s)
}

View File

@@ -40,7 +40,7 @@ impl ToTokens for SliceMacroInput {
let path = &self.path;
tokens.extend(quote! {
::leptos::create_slice(
::leptos::reactive_graph::computed::create_slice(
#root,
|st: &_| st.#path.clone(),
|st: &mut _, n| st.#path = n

View File

@@ -6,7 +6,7 @@ use self::{
slot_helper::{get_slot, slot_to_tokens},
};
use convert_case::{Case::Snake, Casing};
use leptos_hot_reload::parsing::is_component_node;
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use proc_macro_error::abort;
use quote::{quote, quote_spanned, ToTokens};
@@ -319,6 +319,10 @@ pub(crate) fn element_to_tokens(
})
};
let global_class_expr = global_class.map(|class| {
quote! { .class((#class, true)) }
});
let self_closing = is_self_closing(node);
let children = if !self_closing {
element_children_to_tokens(
@@ -348,6 +352,7 @@ pub(crate) fn element_to_tokens(
#name
#children
#attributes
#global_class_expr
})
}
}
@@ -373,8 +378,7 @@ fn is_spread_marker(node: &NodeElement) -> bool {
fn attribute_to_tokens(
tag_type: TagType,
node: &NodeAttribute,
// TODO global_class support
_global_class: Option<&TokenTree>,
global_class: Option<&TokenTree>,
is_custom: bool,
) -> TokenStream {
match node {
@@ -454,7 +458,7 @@ fn attribute_to_tokens(
(name.contains('-') && !name.starts_with("aria-"))
// TODO check: do we actually provide SVG attributes?
// we don't provide statically-checked methods for SVG attributes
|| tag_type == TagType::Svg
|| (tag_type == TagType::Svg && name != "inner_html")
{
let value = attribute_value(node);
quote! {
@@ -463,6 +467,19 @@ fn attribute_to_tokens(
} else {
let key = attribute_name(&node.key);
let value = attribute_value(node);
// special case of global_class and class attribute
if &node.key.to_string() == "class"
&& global_class.is_some()
&& node.value().and_then(value_to_string).is_none()
{
let span = node.key.span();
proc_macro_error::emit_error!(span, "Combining a global class (view! { class = ... }) \
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
};
quote! {
.#key(#value)
}

View File

@@ -1,5 +1,5 @@
use core::num::NonZeroUsize;
use leptos::*;
use leptos::prelude::*;
#[component]
fn Component(

View File

@@ -1,98 +1,131 @@
#[cfg(test)]
use cfg_if::cfg_if;
#[cfg(not(feature = "ssr"))]
pub mod tests {
cfg_if! {
if #[cfg(not(feature = "ssr"))] {
use leptos::{server, server_fn::{codec, ServerFn}, ServerFnError};
use std::any::TypeId;
use leptos::{
server,
server_fn::{codec, ServerFn, ServerFnError},
};
use std::any::TypeId;
#[test]
fn server_default() {
#[server]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
#[test]
fn server_default() {
#[server]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH
.trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
#[test]
fn server_full_legacy() {
#[server(FooBar, "/foo/bar", "Cbor", "my_path")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::Cbor>());
#[test]
fn server_full_legacy() {
#[server(FooBar, "/foo/bar", "Cbor", "my_path")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
);
}
#[test]
fn server_all_keywords() {
#[server(endpoint = "my_path", encoding = "Cbor", prefix = "/foo/bar", name = FooBar)]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::Cbor>());
#[test]
fn server_all_keywords() {
#[server(endpoint = "my_path", encoding = "Cbor", prefix = "/foo/bar", name = FooBar)]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
);
}
#[test]
fn server_mix() {
#[server(FooBar, endpoint = "my_path")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
#[test]
fn server_mix() {
#[server(FooBar, endpoint = "my_path")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
#[test]
fn server_name() {
#[server(name = FooBar)]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(
<FooBar as ServerFn>::PATH.trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
#[test]
fn server_name() {
#[server(name = FooBar)]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(
<FooBar as ServerFn>::PATH.trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
#[test]
fn server_prefix() {
#[server(prefix = "/foo/bar")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric), "/foo/bar/my_server_action");
assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
#[test]
fn server_prefix() {
#[server(prefix = "/foo/bar")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH
.trim_end_matches(char::is_numeric),
"/foo/bar/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
#[test]
fn server_encoding() {
#[server(encoding = "GetJson")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::GetUrl>());
#[test]
fn server_encoding() {
#[server(encoding = "GetJson")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH
.trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::GetUrl>()
);
}
#[test]
fn server_endpoint() {
#[server(endpoint = "/path/to/my/endpoint")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<MyServerAction as ServerFn>::PATH, "/api/path/to/my/endpoint");
assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
#[test]
fn server_endpoint() {
#[server(endpoint = "/path/to/my/endpoint")]
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH,
"/api/path/to/my/endpoint"
);
assert_eq!(
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
);
}
}

View File

@@ -1,4 +1,4 @@
use leptos::{create_runtime, create_rw_signal};
use leptos::prelude::RwSignal;
use leptos_macro::slice;
#[derive(Default)]
@@ -18,9 +18,7 @@ pub struct InnerTuple(String);
#[test]
fn green() {
let _ = create_runtime();
let outer_signal = create_rw_signal(OuterState::default());
let outer_signal = RwSignal::new(OuterState::default());
let (_, _) = slice!(outer_signal.count);

View File

@@ -1,5 +1,5 @@
use leptos::{create_runtime, create_rw_signal};
use leptos_macro::slice;
use leptos::prelude::RwSignal;
#[derive(Default, PartialEq)]
pub struct OuterState {
@@ -14,9 +14,7 @@ pub struct InnerState {
}
fn main() {
let _ = create_runtime();
let outer_signal = create_rw_signal(OuterState::default());
let outer_signal = RwSignal::new(OuterState::default());
let (_, _) = slice!();

View File

@@ -1,31 +1,31 @@
error: unexpected end of input, expected identifier
--> tests/slice/red.rs:21:18
--> tests/slice/red.rs:19:18
|
21 | let (_, _) = slice!();
19 | let (_, _) = slice!();
| ^^^^^^^^
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected `.`
--> tests/slice/red.rs:21:18
|
21 | let (_, _) = slice!(outer_signal);
| ^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
error: unexpected end of input, expected identifier or integer
--> tests/slice/red.rs:23:18
|
23 | let (_, _) = slice!(outer_signal);
| ^^^^^^^^^^^^^^^^^^^^
23 | let (_, _) = slice!(outer_signal.);
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
error: unexpected end of input, expected identifier or integer
--> tests/slice/red.rs:25:18
|
25 | let (_, _) = slice!(outer_signal.);
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
error: unexpected end of input, expected identifier or integer
--> tests/slice/red.rs:27:18
|
27 | let (_, _) = slice!(outer_signal.inner.);
25 | let (_, _) = slice!(outer_signal.inner.);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@@ -1,4 +1,4 @@
use leptos::*;
use leptos::prelude::*;
#[component]
fn missing_scope() {}

View File

@@ -1,19 +1,3 @@
error: return type is incorrect
--> tests/ui/component.rs:4:1
|
4 | fn missing_scope() {}
| ^^^^^^^^^^^^^^^^^^
|
= help: return signature must be `-> impl IntoView`
error: return type is incorrect
--> tests/ui/component.rs:7:1
|
7 | fn missing_return_type() {}
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: return signature must be `-> impl IntoView`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
--> tests/ui/component.rs:10:31
|

View File

@@ -1,11 +1,3 @@
error: return type is incorrect
--> tests/ui/component_absolute.rs:2:1
|
2 | fn missing_return_type() {}
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: return signature must be `-> impl IntoView`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
--> tests/ui/component_absolute.rs:5:31
|

View File

@@ -1,7 +1,8 @@
use leptos::*;
use leptos::prelude::*;
#[server(endpoint = "my_path", FooBar)]
pub async fn positional_argument_follows_keyword_argument() -> Result<(), ServerFnError> {
pub async fn positional_argument_follows_keyword_argument(
) -> Result<(), ServerFnError> {
Ok(())
}

View File

@@ -5,43 +5,43 @@ error: positional argument follows keyword argument
| ^^^^^^
error: keyword argument repeated: `endpoint`
--> tests/ui/server.rs:8:30
--> tests/ui/server.rs:9:30
|
8 | #[server(endpoint = "first", endpoint = "second")]
9 | #[server(endpoint = "first", endpoint = "second")]
| ^^^^^^^^
error: expected string literal
--> tests/ui/server.rs:13:15
--> tests/ui/server.rs:14:15
|
13 | #[server(Foo, Bar)]
14 | #[server(Foo, Bar)]
| ^^^
error: expected string literal
--> tests/ui/server.rs:17:15
--> tests/ui/server.rs:18:15
|
17 | #[server(Foo, Bar, bazz)]
18 | #[server(Foo, Bar, bazz)]
| ^^^
error: expected identifier
--> tests/ui/server.rs:22:10
--> tests/ui/server.rs:23:10
|
22 | #[server("Foo")]
23 | #[server("Foo")]
| ^^^^^
error: expected `,`
--> tests/ui/server.rs:27:14
--> tests/ui/server.rs:28:14
|
27 | #[server(Foo Bar)]
28 | #[server(Foo Bar)]
| ^^^
error: unexpected extra argument
--> tests/ui/server.rs:32:49
--> tests/ui/server.rs:33:49
|
32 | #[server(FooBar, "/foo/bar", "Cbor", "my_path", "extra")]
33 | #[server(FooBar, "/foo/bar", "Cbor", "my_path", "extra")]
| ^^^^^^^
error: Encoding not found.
--> tests/ui/server.rs:37:21
--> tests/ui/server.rs:38:21
|
37 | #[server(encoding = "wrong")]
38 | #[server(encoding = "wrong")]
| ^^^^^^^

View File

@@ -18,6 +18,7 @@ server_fn = { workspace = true }
tracing = { version = "0.1", optional = true }
futures = "0.3"
any_spawner = { workspace = true }
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
# serialization formats

View File

@@ -43,7 +43,7 @@ mod shared;
////! crate that is enabled).
////!
////! ```rust,ignore
////! use leptos::*;
////! use leptos::prelude::*;
////! #[server(ReadFromDB)]
////! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
////! // do some server-only work here to access the database
@@ -118,7 +118,7 @@ mod shared;
////! HTML forms dont support `PUT` or `DELETE`, and they dont support sending JSON. This means that if you use anything
////! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded.
////!
////! The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that
////! The CBOR encoding is supported for historical reasons; an earlier version of server functions used a URL encoding that
////! didnt support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the
////! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of
////! your app is not available.

View File

@@ -48,6 +48,12 @@ impl<T> ArcLocalResource<T> {
if cfg!(feature = "ssr") {
pending().await
} else {
// LocalResources that are immediately available can cause a hydration error,
// because the future *looks* like it is alredy ready (and therefore would
// already have been rendered to html on the server), but in fact was ignored
// on the server. the simplest way to avoid this is to ensure that we always
// wait a tick before resolving any value for a localresource.
any_spawner::Executor::tick().await;
fut.await
}
}
@@ -198,12 +204,23 @@ impl<T> LocalResource<T> {
if cfg!(feature = "ssr") {
pending().await
} else {
// LocalResources that are immediately available can cause a hydration error,
// because the future *looks* like it is alredy ready (and therefore would
// already have been rendered to html on the server), but in fact was ignored
// on the server. the simplest way to avoid this is to ensure that we always
// wait a tick before resolving any value for a localresource.
any_spawner::Executor::tick().await;
fut.await
}
}
};
Self {
data: AsyncDerived::new_unsync(fetcher),
data: if cfg!(feature = "ssr") {
AsyncDerived::new_mock_unsync(fetcher)
} else {
AsyncDerived::new_unsync(fetcher)
},
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}

View File

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

View File

@@ -23,7 +23,7 @@ use leptos::{
/// following the `{..}` operator.
///
/// ```
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_meta::*;
///
/// #[component]

View File

@@ -23,7 +23,7 @@ use leptos::{
/// following the `{..}` operator.
///
/// ```
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_meta::*;
///
/// #[component]

View File

@@ -11,7 +11,7 @@
//! HTML that should be injected into the `<head>` of the HTML document being rendered.
//!
//! ```
//! use leptos::*;
//! use leptos::prelude::*;
//! use leptos_meta::*;
//!
//! #[component]
@@ -173,6 +173,7 @@ pub struct ServerMetaContext {
/// Attributes for the `<body>` element.
pub(crate) body: Sender<String>,
/// Arbitrary elements to be added to the `<head>` as HTML.
#[allow(unused)] // used in SSR
pub(crate) elements: Sender<String>,
}

View File

@@ -8,7 +8,7 @@ use leptos::{
/// head, accepting any of the valid attributes for that tag.
///
/// ```
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_meta::*;
///
/// #[component]

View File

@@ -11,7 +11,7 @@ use leptos::{
/// head to set metadata
///
/// ```
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_meta::*;
///
/// #[component]

View File

@@ -7,7 +7,7 @@ use leptos::{
/// head, accepting any of the valid attributes for that tag.
///
/// ```
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_meta::*;
///
/// #[component]

View File

@@ -7,7 +7,7 @@ use leptos::{
/// head, accepting any of the valid attributes for that tag.
///
/// ```
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_meta::*;
///
/// #[component]

View File

@@ -8,7 +8,7 @@ use leptos::{
/// head that loads a stylesheet from the URL given by the `href` property.
///
/// ```
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_meta::*;
///
/// #[component]

View File

@@ -73,7 +73,7 @@ where
/// `<Title formatter=.../>` that will wrap each of the text values of `<Title/>` components created lower in the tree.
///
/// ```
/// use leptos::*;
/// use leptos::prelude::*;
/// use leptos_meta::*;
///
/// #[component]
@@ -181,20 +181,24 @@ impl TitleView {
}
}
#[allow(dead_code)] // TODO these should be used to rebuild the attributes, I guess
struct TitleViewState {
el: HtmlTitleElement,
formatter: Option<Formatter>,
text: Option<TextProp>,
// effect is stored in the view state to keep it alive until rebuild
#[allow(dead_code)]
effect: RenderEffect<Oco<'static, str>>,
}
impl Render<Dom> for TitleView {
type State = TitleViewState;
fn build(self) -> Self::State {
fn build(mut self) -> Self::State {
let el = self.el();
let meta = self.meta;
if let Some(formatter) = self.formatter.take() {
*meta.title.formatter.write().or_poisoned() = Some(formatter);
}
if let Some(text) = self.text.take() {
*meta.title.text.write().or_poisoned() = Some(text);
}
let effect = RenderEffect::new({
let el = el.clone();
move |prev| {
@@ -207,16 +211,11 @@ impl Render<Dom> for TitleView {
text
}
});
TitleViewState {
el,
formatter: self.formatter,
text: self.text,
effect,
}
TitleViewState { effect }
}
fn rebuild(self, _state: &mut Self::State) {
// TODO should this rebuild?
fn rebuild(self, state: &mut Self::State) {
*state = self.build();
}
}
@@ -257,12 +256,18 @@ impl RenderHtml<Dom> for TitleView {
}
fn hydrate<const FROM_SERVER: bool>(
self,
mut self,
_cursor: &Cursor<Dom>,
_position: &PositionState,
) -> Self::State {
let el = self.el();
let meta = self.meta;
if let Some(formatter) = self.formatter.take() {
*meta.title.formatter.write().or_poisoned() = Some(formatter);
}
if let Some(text) = self.text.take() {
*meta.title.text.write().or_poisoned() = Some(text);
}
let effect = RenderEffect::new({
let el = el.clone();
move |prev| {
@@ -276,12 +281,7 @@ impl RenderHtml<Dom> for TitleView {
text
}
});
TitleViewState {
el,
formatter: self.formatter,
text: self.text,
effect,
}
TitleViewState { effect }
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "next_tuple"
version = "0.1.0-alpha"
version = "0.1.0-beta"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -8,9 +8,9 @@ codegen-units = 1
lto = true
[dependencies]
leptos = { version = "0.6.12", features = ["csr"] }
leptos_meta = { version = "0.6.12", features = ["csr"] }
leptos_router = { version = "0.6.12", features = ["csr"] }
leptos = { version = "0.6.13", features = ["csr"] }
leptos_meta = { version = "0.6.13", features = ["csr"] }
leptos_router = { version = "0.6.13", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -14,7 +14,7 @@ leptos_router = { path = "../../../router", features = ["csr"] }
log = "0.4"
console_error_panic_hook = "0.1"
console_log = "1"
gloo-net = "0.5"
gloo-net = "0.6"
gloo-storage = "0.3"
serde = "1.0"
thiserror = "1.0"

View File

@@ -37,7 +37,7 @@ pub fn make_openapi_call_via_gpt(message:String) -> ChatCompletionParameters {
// This name will be given to the OpenAI API as part of our functions
let name = operation.operation_id.clone().expect("Each operation to have an operation id");
// we'll use the descrition
// we'll use the description
let desc = operation.description.clone().expect("Each operation to have a description, this is how GPT knows what the functiond does and it is helpful for calling it.");
let mut required_list = vec![];
let mut properties = serde_json::Map::new();

View File

@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["app", "frontend", "ids", "server","e2e"]
members = ["app", "frontend", "ids", "server", "e2e"]
# need to be applied only to wasm build
[profile.release]
@@ -13,34 +13,38 @@ leptos = { version = "0.6.9", features = ["nightly"] }
leptos_meta = { version = "0.6.9", features = ["nightly"] }
leptos_router = { version = "0.6.9", features = ["nightly"] }
leptos_axum = { version = "0.6.9" }
leptos-use = {version = "0.10.5"}
leptos-use = { version = "0.10.5" }
axum = "0.7"
axum-server = {version = "0.6", features = ["tls-rustls"]}
axum-extra = { version = "0.9.2", features=["cookie"]}
axum-server = { version = "0.6", features = ["tls-rustls"] }
axum-extra = { version = "0.9.2", features = ["cookie"] }
cfg-if = "1"
console_error_panic_hook = "0.1.7"
console_log = "1"
http = "1"
ids = {path="./ids"}
ids = { path = "./ids" }
# this goes to this personal branch because of https://github.com/ory/sdk/issues/325#issuecomment-1960834676
ory-kratos-client = {git="https://github.com/sjud/kratos-client-rust"}
ory-keto-client = {version = "0.11.0-alpha.0"}
reqwest = { version = "0.11.24", features = ["json","cookies"] }
ory-kratos-client = { git = "https://github.com/sjud/kratos-client-rust" }
ory-keto-client = { version = "0.11.0-alpha.0" }
reqwest = { version = "0.12", features = ["json", "cookies"] }
serde = "1.0.197"
serde_json = "1.0.114"
sqlx = {version= "0.7.3", features=["runtime-tokio","sqlite","macros"]}
sqlx = { version = "0.7.3", features = ["runtime-tokio", "sqlite", "macros"] }
thiserror = "1"
time = "0.3.34"
tokio = { version = "1.33.0", features = ["full"] }
tower = { version = "0.4.13", features = ["full"] }
tower-http = { version = "0.5", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = {version="0.3.18", features=["env-filter"]}
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = "2.5.0"
uuid = {version = "1.7.0", features=["v4","serde"]}
uuid = { version = "1.7.0", features = ["v4", "serde"] }
wasm-bindgen = "0.2.92"
web-sys = {version = "0.3.69", features=["HtmlDocument","HtmlFormElement","FormData"]}
web-sys = { version = "0.3.69", features = [
"HtmlDocument",
"HtmlFormElement",
"FormData",
] }
# See https://github.com/akesson/cargo-leptos for documentation of all the parameters.

View File

@@ -93,7 +93,7 @@ pub fn kratos_html(node: UiNode, body: RwSignal<HashMap<String, String>>) -> imp
body.update(|map| {
_ = map.insert(name.clone(), value.clone());
});
// this expects the identifer to be an email, but it could be telelphone etc so code is extra fragile
// this expects the identifier to be an email, but it could be telephone etc so code is extra fragile
view! {<input type="hidden" value=value name=name /> }.into_view()
}
}

View File

@@ -6,19 +6,21 @@ edition = "2021"
[dev-dependencies]
anyhow = "1.0.72"
async-trait = "0.1.72"
cucumber = {version="0.20.2",features=["tracing","macros"]}
cucumber = { version = "0.20.2", features = ["tracing", "macros"] }
pretty_assertions = "1.4.0"
serde_json = "1.0.104"
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] }
url = "2.4.0"
reqwest = "0.11.25"
reqwest = "0.12"
tracing = "0.1.40"
chromiumoxide = {version = "0.5.7", default-features = false, features=["tokio-runtime"]}
chromiumoxide = { version = "0.5.7", default-features = false, features = [
"tokio-runtime",
] }
ids.workspace = true
fake = "2.9.2"
tokio-tungstenite = "0.21.0"
futures-util = "0.3.30"
uuid = {version="1.7.0",features=["serde"]}
uuid = { version = "1.7.0", features = ["serde"] }
once_cell = "1.19.0"
futures = "0.3.30"
@@ -28,13 +30,9 @@ harness = false # Allow Cucumber to print output instead of libtest
[features]
#vscode thing to get autocomplete
ssr=[]
ssr = []
[dependencies]
once_cell = "1.19.0"
regex = "1.10.3"
serde.workspace = true

View File

@@ -27,7 +27,7 @@ pub static LOGOUT_BUTTON_ID: &'static str = "logout_button_id";
pub static LOGIN_BUTTON_ID: &'static str = "login_button_id";
/// This function is for use in kratos_html, it takes the name of the input node and it
/// matches it according to what we've specified in the kratos schema file. If we change the schema.
/// I.e use a phone instead of an email, the identifer id will change and break tests that expect an email.
/// I.e use a phone instead of an email, the identifier id will change and break tests that expect an email.
/// i.e use oidc instead of password, as auth method... that will break tests too.
/// Which is good.
pub fn match_name_to_id(name: String) -> &'static str {

View File

@@ -27,39 +27,39 @@ tower-http = { version = "0.5", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "1" }
sqlx = { version = "0.7", features = [
"runtime-tokio-rustls",
"sqlite",
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
axum_session_auth = { version = "0.12", features = [
"sqlite-rustls",
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.12", features = [
"sqlite-rustls",
"sqlite-rustls",
], optional = true }
async-trait = { version = "0.1.64", optional = true }
reqwest = { version = "0.11", optional = true, features = ["json"] }
reqwest = { version = "0.12", optional = true, features = ["json"] }
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:serde_json",
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:reqwest",
"dep:oauth2",
"dep:axum_session_auth",
"dep:axum_session",
"dep:async-trait",
"dep:sqlx",
"dep:rand",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:serde_json",
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:reqwest",
"dep:oauth2",
"dep:axum_session_auth",
"dep:axum_session",
"dep:async-trait",
"dep:sqlx",
"dep:rand",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]

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