Compare commits

..

90 Commits

Author SHA1 Message Date
Greg Johnston
90c6249067 examples: better practice for view types in todos 2023-04-23 15:15:01 -04:00
Greg Johnston
c74b15b120 docs: add section on WASM binary size 2023-04-23 15:07:48 -04:00
Craig Rodrigues
9a4f3ab08c chore: specify dependency version for cached (#929) 2023-04-22 17:51:40 -04:00
Greg Johnston
a0935c169e docs: add some content on server-side rendering (#930) 2023-04-22 15:15:48 -04:00
yuuma03
0e2181fb90 fix: allow nested slots (#928) 2023-04-22 14:14:01 -04:00
Greg Johnston
732ec14302 docs: add use of batch to avoid BorrowMut panic 2023-04-22 07:03:10 -04:00
agilarity
ec95060b6e fix: features related compile error (#919)
`cargo make test` sets the --all-features flag by default. This change
clears it.
2023-04-22 06:50:35 -04:00
J
689afec26e docs: fixed typo in interlude_styling.md (#924) 2023-04-22 06:49:15 -04:00
J
bbf23ea40a docs: removed extra unused code blocks in form.md (#923) 2023-04-22 06:48:28 -04:00
J
34e0a8e47d docs: fixed a minor typo in async readme (#921) 2023-04-22 06:47:44 -04:00
Ben Wishovich
81f330e888 feat: add thorough tracing throughout (#908) 2023-04-22 06:47:11 -04:00
Greg Johnston
e5d657dd55 fix: panic when creating nested StoredValue (#920) 2023-04-22 06:44:25 -04:00
Greg Johnston
f919127a7e fix some issues with animated routing (#889) 2023-04-21 15:33:14 -04:00
Greg Johnston
2001bd808f examples: fix broken counters tests (#915) 2023-04-21 15:26:18 -04:00
yuuma03
f51857cedc feat: add slots (closes #769) (#909) 2023-04-21 14:36:38 -04:00
Greg Johnston
f3b8d27c4f change: add window_event_listener_untyped and deprecate window_event_listener pending 0.3.0 (#913) 2023-04-21 14:14:35 -04:00
Greg Johnston
d3a577c365 cargo fmt 2023-04-21 12:45:08 -04:00
Greg Johnston
b80f9e3871 fix: issue with ordering of class attribute and class=("fancy-name-200", true) (closes #907) (#914) 2023-04-21 12:42:35 -04:00
Greg Johnston
328d42656d docs: compile error on mutually-exclusive features (#911) 2023-04-21 12:25:21 -04:00
Logan B. Nielsen
d3d2cbed7e feat: add typed window event listeners (#910) 2023-04-21 11:43:11 -04:00
agilarity
d6f7aedec1 CI: use cargo make to run tests for examples (#904) 2023-04-21 10:33:12 -04:00
Daniel Santana
7a5a776cb9 feat: get_untracked for node_ref. (#902) 2023-04-19 20:09:54 -04:00
Greg Johnston
06f782aa13 perf: improve router performance on server by calculating route branches once (#898) 2023-04-19 20:09:29 -04:00
Greg Johnston
6b825fec37 fix: erroneous non-reactive access warning in undelegated events (#900) 2023-04-19 20:09:05 -04:00
Greg Johnston
b452d8af40 feat: add ability to mutate resources (closes #856) (#886) 2023-04-19 11:40:46 -04:00
Daniel Santana
e96f1d2129 feat: impl Serialize/Deserialize for ParamsMap (closes #892) (#895) 2023-04-19 06:19:53 -04:00
OvermindDL1
72d6af9c84 fix: use once_cell crate until OnceLock stabilized (closes #890)
* Fixes #890 that was using OnceLock, which is nightly only, by adding the once_cell crate as a dependency.

* Make `cargo fmt` happy
2023-04-18 16:31:04 -04:00
Filip Dutescu
8198cd0b68 chore(readme): add link to Matrix bridge (#894)
While the project offers a [Matrix][matrix] bridge, it is nowhere shown.
One would need to join the [Discord][discord] server and search through
it to find it.

To make it easier to join through [Matrix][matrix], add a badge in the
project `README.md`.

[matrix]: https://matrix.org/
[discord]: https://discord.com/

Fixes: #893

Signed-off-by: Filip Dutescu <filip@hucksy.dev>
2023-04-18 15:30:00 -04:00
Greg Johnston
fe68b47ba2 perf: tiny optimization on primitive child values (#887) 2023-04-17 22:09:10 -04:00
Greg Johnston
384d39543c fix: dispose of scope when server fns return error (closes #862) (#888) 2023-04-17 22:08:47 -04:00
agilarity
225e62d12f examples: split counter without macros web test (#884) 2023-04-17 20:26:31 -04:00
Greg Johnston
3905a2aa60 docs: SSR modes 2023-04-17 17:00:52 -04:00
Greg Johnston
ff6ce2dac0 docs: add SSR mode videos 2023-04-17 16:03:36 -04:00
Greg Johnston
16675cbff2 docs: add chapter on styling 2023-04-17 11:50:50 -04:00
HJin.me
9524c6e289 fix: <For/> rendering error in SSR InOrder/Async Mode (#879) 2023-04-17 10:48:07 -04:00
Mark Catley
bc316c648c feat: add expect_context function (#864)
Most of the time when using use_context it would be a bug if the context
wasn't present and appropriate to panic. This is a convenience function
that has that behavior.
2023-04-17 10:47:50 -04:00
Matt Crane
6753ba21c4 fix: allow server functions to work with non-Cargo build systems with SERVER_FN_OVERRIDE_KEY env var (#878) 2023-04-17 08:46:32 -04:00
Greg Johnston
efbe32e081 feat: add non-animation base classes to <AnimatedOutlet/> and <AnimatedRoutes/> (#877) 2023-04-17 08:12:22 -04:00
Kamil Ogórek
55fd6d44f9 docs: Add per-project toolchain override readme (#876) 2023-04-16 16:30:20 -04:00
Mustafa Zaki Assagaf
90972f2d94 fix: updated nix flakes lock files on session auth axum examples to fix once_cell doesn't compile (#872) 2023-04-15 13:32:59 -04:00
Greg Johnston
7382c7e51c feat: add the ability to specify animations on route transitions (#736) 2023-04-14 18:20:42 -04:00
Greg Johnston
8a6d129575 examples: fix error handling in fetch example (#870) 2023-04-14 16:13:14 -04:00
Stackingttv
e20c77710d docs: fixed typo in life cycle docs (#869) 2023-04-14 15:12:18 -04:00
Greg Johnston
93da88eac0 feat: add ability to set node_ref and pass additional attributes to <Form/> and friends (#853) 2023-04-14 14:25:52 -04:00
agilarity
5072539917 examples: fix counter_without_macros test (#863) 2023-04-14 14:06:53 -04:00
Chris Roth
78c59df1d1 docs: fix match statement (#860) 2023-04-14 14:05:21 -04:00
Greg Johnston
75e40eafb2 docs: add "Life Cycle of a Page Load" 2023-04-14 13:30:53 -04:00
Álvaro Mondéjar
274a1ac5f0 Remove & at the end of params queries (#854) 2023-04-12 17:04:22 -04:00
Greg Johnston
17040a4af4 fix: custom events in SSR mode (#852) 2023-04-12 13:21:36 -04:00
Greg Johnston
b09a5f905e docs: emit error when trying to combine global class and dynamic class in a bugged way (#850) 2023-04-11 21:15:07 -04:00
Greg Johnston
683511f311 clippy 2023-04-11 14:37:54 -04:00
Greg Johnston
151c58733b docs: clean up methods documentation 2023-04-11 14:37:12 -04:00
Greg Johnston
012ff56cd6 fix static text nodes with curly braces in SSR (#849) 2023-04-11 12:46:32 -04:00
Nova
493c805993 feat: Trigger primitive and reactive-system cleanups (#838) 2023-04-10 17:47:52 -04:00
Greg Johnston
764192af36 feat: allow multiple HTTP request methods/verbs (#695) 2023-04-10 16:42:15 -04:00
Greg Johnston
f969fd7eff fix: don't entity-encode HTML special characters inside <script> or <style> (closes #837) (#846) 2023-04-10 13:15:15 -04:00
Greg Johnston
2c7ee0d415 feat: rustls feature for reqwest and any other relevant dependencies (#842) 2023-04-10 13:15:00 -04:00
Snêu
5430c78e18 docss: correct broken MaybeSignal link (#840) 2023-04-10 07:37:41 -04:00
Greg Johnston
6b052557d1 fix: correct todo_app_sqlite README (closes #845) 2023-04-10 07:17:46 -04:00
Nova
70f3edb0f5 fix: fix leaks in memos, and in scope parent tracking (#841) 2023-04-09 16:36:53 -04:00
Greg Johnston
4e1f963750 Merge pull request #831 from novacrazy/main
Various optimizations, size reductions and stability improvements
2023-04-08 09:04:13 -04:00
novacrazy
3c3d3b33f1 Remove unnecessary into 2023-04-07 17:41:27 -05:00
novacrazy
be7b9eea25 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-07 14:09:10 -05:00
Greg Johnston
016ad6b7a6 feat: make __Props imports unnecessary (closes #746) (#828) 2023-04-07 15:06:10 -04:00
novacrazy
60b96c9118 Couple more inline tweaks 2023-04-07 05:28:50 -05:00
novacrazy
7ccb2d9f44 Simplify SsrMode enum 2023-04-07 05:10:55 -05:00
novacrazy
2c2090a194 Return Cow from as_value_string 2023-04-07 05:09:49 -05:00
novacrazy
de9b2998ac More inlining 2023-04-07 05:09:24 -05:00
novacrazy
29b81a3d50 Another round of inlining 2023-04-07 01:44:18 -05:00
novacrazy
5bc0d89ce7 Cleanup Comment::new 2023-04-07 00:52:35 -05:00
novacrazy
342b10c232 Use Cow for ErrorKey 2023-04-07 00:52:23 -05:00
novacrazy
ba9d3c1602 Another round of inlining 2023-04-07 00:52:11 -05:00
novacrazy
d3b3ce6980 Cleanup benchmarks 2023-04-06 21:56:24 -05:00
novacrazy
4b79a91287 Add profile.release to many examples 2023-04-06 21:53:52 -05:00
novacrazy
de06c9b2ca Fix Box<dyn> casts 2023-04-06 21:52:25 -05:00
novacrazy
84c7d00ea9 Use Cow<'static, str> for Attributes 2023-04-06 21:52:11 -05:00
novacrazy
8f5ae0054d Second round of inlining 2023-04-06 21:39:29 -05:00
Nova
374f0c4e27 Merge branch 'leptos-rs:main' into main 2023-04-06 21:31:41 -05:00
novacrazy
a6170f4da9 First round of inlining 2023-04-06 21:02:40 -05:00
novacrazy
578dd5ef35 Convert bubbles to associated const for more reliable const-eval and dead-code elimination 2023-04-06 20:55:18 -05:00
novacrazy
934a131deb Pull out non-generic code from leptos_dom
Avoids duplicate codegen
2023-04-06 20:52:13 -05:00
novacrazy
5bc1c36e67 Pull out non-generic code in reactive core 2023-04-06 20:32:59 -05:00
novacrazy
b1b9853f92 Replace with_scope_property with push_scope_property
Avoids duplicate codegen
2023-04-06 20:26:58 -05:00
novacrazy
5d6a083d1d Fix nested batching and improve batch/untrack behavior 2023-04-06 20:22:58 -05:00
novacrazy
b51da35a9a Remove allocation in ScopeDisposer 2023-04-05 21:21:22 -05:00
novacrazy
164dcd1b97 Cleanup signal clone impl 2023-04-05 21:19:04 -05:00
novacrazy
c0964c2b01 Fast path for linear deeply nested children 2023-04-05 21:17:37 -05:00
novacrazy
af5b226e53 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-05 19:30:06 -05:00
novacrazy
3a1db3a191 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-05 08:39:49 -05:00
novacrazy
54370e3153 Reduce size of RuntimeId when slotmap is not used 2023-04-04 19:26:57 -05:00
151 changed files with 5497 additions and 1361 deletions

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ Cargo.lock
**/*.rs.bk
.DS_Store
.idea
.direnv
.envrc

View File

@@ -49,6 +49,7 @@ dependencies = [
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
{ name = "check", path = "examples/session_auth_axum" },
{ name = "check", path = "examples/slots" },
{ name = "check", path = "examples/ssr_modes" },
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
@@ -75,8 +76,20 @@ command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
[tasks.test-examples]
description = "Run all unit and web tests for examples"
cwd = "examples"
command = "cargo"
args = ["make", "test-unit-and-web"]
[tasks.verify-examples]
description = "Run all quality checks and tests for examples"
cwd = "examples"
command = "cargo"
args = ["make", "verify-flow"]
[env]
RUSTFLAGS=""
RUSTFLAGS = ""
[env.github-actions]
RUSTFLAGS="-D warnings"
RUSTFLAGS = "-D warnings"

View File

@@ -6,6 +6,7 @@
[![crates.io](https://img.shields.io/crates/v/leptos.svg)](https://crates.io/crates/leptos)
[![docs.rs](https://docs.rs/leptos/badge.svg)](https://docs.rs/leptos)
[![Discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord)](https://discord.gg/YdRAhS7eQB)
[![Matrix](https://img.shields.io/badge/Matrix-leptos-grey?logo=matrix&labelColor=white&logoColor=black)](https://matrix.to/#/#leptos:matrix.org)
# Leptos
@@ -65,9 +66,9 @@ Here are some resources for learning more about Leptos:
## `nightly` Note
Most of the examples assume youre using `nightly` Rust.
Most of the examples assume youre using `nightly` version of Rust. For this, you can either set your toolchain globally or on per-project basis.
To set up your Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you havent already)
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you havent already):
```
rustup toolchain install nightly
@@ -75,6 +76,14 @@ rustup default nightly
rustup target add wasm32-unknown-unknown
```
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
```toml
[toolchain]
channel = "nightly"
targets = ["wasm32-unknown-unknown"]
```
If youre on `stable`, note the following:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`

View File

@@ -10,8 +10,8 @@ fn leptos_deep_creation(b: &mut Bencher) {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
@@ -34,9 +34,8 @@ fn leptos_deep_update(b: &mut Bencher) {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
@@ -242,9 +241,8 @@ fn l021_deep_creation(b: &mut Bencher) {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
@@ -266,9 +264,8 @@ fn l021_deep_update(b: &mut Bencher) {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
@@ -444,9 +441,8 @@ fn sycamore_deep_creation(b: &mut Bencher) {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));
@@ -465,9 +461,8 @@ fn sycamore_deep_update(b: &mut Bencher) {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));

View File

@@ -28,6 +28,52 @@ let (a, set_a) = create_signal(cx, 0);
let b = move || a () > 5;
```
### Nested signal updates/reads triggering panic
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let resources = create_rw_signal(cx, HashMap::new());
let update = move |id: usize| {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(cx, 0))
.update(|amount| *amount += 1)
})
};
view! { cx,
<div>
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
<button on:click=move |_| update(1)>"+"</button>
</div>
}
}
```
Clicking the button twice will cause a panic, because of the nested signal *read*. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
You can fix this fairly easily by using the [`Scope::batch()`](https://docs.rs/leptos/latest/leptos/struct.Scope.html#method.batch) method:
```rust
let update = move |id: usize| {
cx.batch(move || {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(cx, 0))
.update(|amount| *amount += 1)
})
});
};
```
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
## Templates and the DOM
### `<input value=...>` doesn't update or stops updating

View File

@@ -27,22 +27,24 @@
- [Params and Queries](./router/18_params_and_queries.md)
- [`<A/>`](./router/19_a.md)
- [`<Form/>`](./router/20_form.md)
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
- [Interlude: Styling](./interlude_styling.md)
- [Metadata]()
- [SSR]()
- [Models of SSR]()
- [`cargo-leptos`]()
- [Hydration Footguns]()
- [Server Side Rendering](./ssr/README.md)
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
- [The Life of a Page Load](./ssr/22_life_cycle.md)
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
- [Hydration Footguns](./ssr/24_hydration_bugs.md)
- [Server Functions]()
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
- [Headers]()
- [Cookies]()
- [Server Functions]()
- [Building Full-Stack Apps]()
- [Actions]()
- [Forms]()
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size]()
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)

View File

@@ -0,0 +1,58 @@
# Appendix: Optimizing WASM Binary Size
One of the primary downsides of deploying a Rust/WebAssembly frontend app is that splitting a WASM file into smaller chunks to be dynamically loaded is significantly more difficult than splitting a JavaScript bundle. There have been experiments like [`wasm-split`](https://emscripten.org/docs/optimizing/Module-Splitting.html) in the Emscripten ecosystem but at present theres no way to split and dynamically load a Rust/`wasm-bindgen` binary. This means that the whole WASM binary needs to be loaded before your app becomes interactive. Because the WASM format is designed for streaming compilation, WASM files are much faster to compile per kilobyte than JavaScript files. (For a deeper look, you can [read this great article from the Mozilla team](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/) on streaming WASM compilation.)
Still, its important to ship the smallest WASM binary to users that you can, as it will reduce their network usage and make your app interactive as quickly as possible.
So what are some practical steps?
## Things to Do
1. Make sure youre looking at a release build. (Debug builds are much, much larger.)
2. Add a release profile for WASM that optimizes for size, not speed.
For a `cargo-leptos` project, for example, you can add this to your `Cargo.toml`:
```toml
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
# ....
[package.metadata.leptos]
# ....
lib-profile-release = "wasm-release"
```
This will hyper-optimize the WASM for your release build for size, while keeping your server build optimized for speed. (For a pure client-rendered app without server considerations, just use the `[profile.wasm-release]` block as your `[profile.release]`.)
3. Always serve compressed WASM in production. WASM tends to compress very well, typically shrinking to less than 50% its uncompressed size, and its trivial to enable compression for static files being served from Actix or Axum.
4. If youre using nightly Rust, you can rebuild the standard library with this same profile rather than the prebuilt standard library thats distributed with the `wasm32-unknown-unknown` target.
To do this, create a file in your project at `.cargo/config.toml`
```toml
[unstable]
build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]
```
5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`s functionality, but typically optimizes for size over speed.
## Things to Avoid
There are certain crates that tend to inflate binary sizes. For example, the `regex` crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!) In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what `leptos_router` does on the few occasions it needs a regular expression.)
In general, Rusts commitment to runtime performance is sometimes at odds with a commitment to a small binary. For example, Rust monomorphizes generic functions, meaning it creates a distinct copy of the function for each generic type its called with. This is significantly faster than dynamic dispatch, but increases binary size. Leptos tries to balance runtime performance with binary size considerations pretty carefully; but you might find that writing code that uses many generics tends to increase binary size. For example, if you have a generic component with a lot of code in its body and call it with four different types, remember that the compiler could include four copies of that same code. Refactoring to use a concrete inner function or helper can often maintain performance and ergonomics while reducing binary size.
## A Final Thought
Remember that in a server-rendered app, JS bundle size/WASM binary size affects only _one_ thing: time to interactivity on the first load. This is very important to a good user experience—nobody wants to click a button three times and have it do nothing because the interactive code is still loading—but it is not the only important measure.
Its especially worth remembering that streaming in a single WASM binary means all subsequent navigations are nearly instantaneous, depending only on any additional data loading. Precisely because your WASM binary is _not_ bundle split, navigating to a new route does not require loading additional JS/WASM, as it does in nearly every JavaScript framework. Is this copium? Maybe. Or maybe its just an honest trade-off between the two approaches!
Always take the opportunity to optimize the low-hanging fruit in your application. And always test your app under real circumstances with real user network speeds and devices before making any heroic efforts.

View File

@@ -26,11 +26,11 @@ let b = create_resource(cx, count2, |count| async move { load_b(count).await });
view! { cx,
<h1>"My Data"</h1>
{move || match (a.read(cx), b.read(cx)) {
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
(Some(a), Some(b)) => view! { cx,
<ShowA a/>
<ShowA b/>
}.into_view(cx)
}.into_view(cx),
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
}}
}
```

View File

@@ -1,7 +1,7 @@
# Working with `async`
So far weve only been working with synchronous users interfaces: You provide some input,
the app immediately process it and updates the interface. This is great, but is a tiny
the app immediately processes it and updates the interface. This is great, but is a tiny
subset of what web applications do. In particular, most web apps have to deal with some kind
of asynchronous data loading, usually loading something from an API.

View File

@@ -0,0 +1,112 @@
# Interlude: Styling
Anyone creating a website or application soon runs into the question of styling. For a small app, a single CSS file is probably plenty to style your user interface. But as an application grows, many developers find that plain CSS becomes increasingly hard to manage.
Some frontend frameworks (like Angular, Vue, and Svelte) provide built-in ways to scope your CSS to particular components, making it easier to manage styles across a whole application without styles meant to modify one small component having a global effect. Other frameworks (like React or Solid) dont provide built-in CSS scoping, but rely on libraries in the ecosystem to do it for them. Leptos is in this latter camp: the framework itself has no opinions about CSS at all, but provides a few tools and primitives that allow others to build styling libraries.
Here are a few different approaches to styling your Leptos app, other than plain CSS.
## TailwindCSS: Utility-first CSS
[TailwindCSS](https://tailwindcss.com/) is a popular utility-first CSS library. It allows you to style your application by using inline utility classes, with a custom CLI tool that scans your files for Tailwind class names and bundles the necessary CSS.
This allows you to write components like this:
```rust
#[component]
fn Home(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<main class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
<button
class="bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
on:click=move |_| set_count.update(|count| *count += 1)
>
{move || if count() == 0 {
"Click me!".to_string()
} else {
count().to_string()
}}
</button>
</main>
}
}
```
It can be a little complicated to set up the Tailwind integration at first, but you can check out our two examples of how to use Tailwind with a [client-side-rendered `trunk` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr_trunk) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind). `cargo-leptos` also has some [built-in Tailwind support](https://github.com/leptos-rs/cargo-leptos#site-parameters) that you can use as an alternative to Tailwinds CLI.
## Stylers: Compile-time CSS Extraction
[Stylers](https://github.com/abishekatp/stylers) is a compile-time scoped CSS library that lets you declare scoped CSS in the body of your component. Stylers will extract this CSS at compile time into CSS files that you can then import into your app, which means that it doesnt add anything to the WASM binary size of your application.
This allows you to write components like this:
```rust
use stylers::style;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let styler_class = style! { "App",
#two{
color: blue;
}
div.one{
color: red;
content: raw_str(r#"\hello"#);
font: "1.3em/1.2" Arial, Helvetica, sans-serif;
}
div {
border: 1px solid black;
margin: 25px 50px 75px 100px;
background-color: lightblue;
}
h2 {
color: purple;
}
@media only screen and (max-width: 1000px) {
h3 {
background-color: lightblue;
color: blue
}
}
};
view! { cx, class = styler_class,
<div class="one">
<h1 id="two">"Hello"</h1>
<h2>"World"</h2>
<h2>"and"</h2>
<h3>"friends!"</h3>
</div>
}
}
```
## Styled: Runtime CSS Scoping
[Styled](https://github.com/eboody/styled) is a runtime scoped CSS library that integrates well with Leptos. It lets you declare scoped CSS in the body of your component function, and then applies those styles at runtime.
```rust
use styled::style;
#[component]
pub fn MyComponent(cx: Scope) -> impl IntoView {
let styles = style!(
div {
background-color: red;
color: white;
}
);
styled::view! { cx, styles,
<div>"This text should be red with white text."</div>
}
}
```
## Contributions Welcome
Leptos has no opinions on how you style your website or app, but were very happy to provide support to any tools youre trying to create to make it easier. If youre working on a CSS or styling approach that youd like to add to this list, please let us know!

View File

@@ -23,8 +23,6 @@ async fn fetch_results() {
// some async function to fetch our search results
}
#[component]
pub fn Search(cx: Scope) -> impl IntoView {
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
// reactive access to URL query strings

View File

@@ -0,0 +1,37 @@
# Introducing `cargo-leptos`
So far, weve just been running code in the browser and using Trunk to coordinate the build process and run a local development process. If were going to add server-side rendering, well need to run our application code on the server as well. This means well need to build two separate binaries, one compiled to native code and running the server, the other compiled to WebAssembly (WASM) and running in the users browser. Additionally, the server needs to know how to serve this WASM version (and the JavaScript required to initialize it) to the browser.
This is not an insurmountable task but it adds some complication. For convenience and an easier developer experience, we built the [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool. `cargo-leptos` basically exists to coordinate the build process for your app, handling recompiling the server and client halves when you make changes, and adding some built-in support for things like Tailwind, SASS, and testing.
Getting started is pretty easy. Just run
```bash
cargo install cargo-leptos
```
And then to create a new project, you can run either
```bash
# for an Actix template
cargo leptos new --git leptos-rs/start
```
or
```bash
# for an Axum template
cargo leptos new --git leptos-rs/start-axum
```
Now `cd` into the directory youve created and run
```bash
cargo leptos watch
```
Once your app has compiled you can open up your browser to [`http://localhost:3000`](http://localhost:3000) to see it.
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).
But what exactly is happening when you open our browser to `localhost:3000`? Well, read on to find out.

View File

@@ -0,0 +1,43 @@
# The Life of a Page Load
Before we get into the weeds it might be helpful to have a higher-level overview. What exactly happens between the moment you type in the URL of a server-rendered Leptos app, and the moment you click a button and a counter increases?
Im assuming some basic knowledge of how the Internet works here, and wont get into the weeds about HTTP or whatever. Instead, Ill try to show how different parts of the Leptos APIs map onto each part of the process.
This description also starts from the premise that your app is being compiled for two separate targets:
1. A server version, often running on Actix or Axum, compiled with the Leptos `ssr` feature
2. A browser version, compiled to WebAssembly (WASM) with the Leptos `hydrate` feature
The [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool exists to coordinate the process of compiling your app for these two different targets.
## On the Server
- Your browser makes a `GET` request for that URL to your server. At this point, the browser knows almost nothing about the page thats going to be rendered. (The question “How does the browser know where to ask for the page?” is an interesting one, but out of the scope of this tutorial!)
- The server receives that request, and checks whether it has a way to handle a `GET` request at that path. This is what the `.leptos_routes()` methods in [`leptos_axum`](https://docs.rs/leptos_axum/0.2.5/leptos_axum/trait.LeptosRoutes.html) and [`leptos_actix`](https://docs.rs/leptos_actix/0.2.5/leptos_actix/trait.LeptosRoutes.html) are for. When the server starts up, these methods walk over the routing structure you provide in `<Routes/>`, generating a list of all possible routes your app can handle and telling the servers router “for each of these routes, if you get a request... hand it off to Leptos.”
- The server sees that this route can be handled by Leptos. So it renders your root component (often called something like `<App/>`), providing it with the URL thats being requested and some other data like the HTTP headers and request metadata.
- Your application runs once on the server, building up an HTML version of the component tree that will be rendered at that route. (Theres more to be said here about resources and `<Suspense/>` in the next chapter.)
- The server returns this HTML page, also injecting information on how to load the version of your app that has been compiled to WASM so that it can run in the browser.
> The HTML page thats returned is essentially your app, “dehydrated” or “freeze-dried”: it is HTML without any of the reactivity or event listeners youve added. The browser will “rehydrate” this HTML page by adding the reactive system and attaching event listeners to that server-rendered HTML. Hence the two feature flags that apply to the two halves of this process: `ssr` on the server for “server-side rendering”, and `hydrate` in the browser for that process of rehydration.
## In the Browser
- The browser receives this HTML page from the server. It immediately goes back to the server to begin loading the JS and WASM necessary to run the interactive, client side version of the app.
- In the meantime, it renders the HTML version.
- When the WASM version has reloaded, it does the same route-matching process that the server did. Because the `<Routes/>` component is identical on the server and in the client, the browser version will read the URL and render the same page that was already returned by the server.
- During this initial “hydration” phase, the WASM version of your app doesnt re-create the DOM nodes that make up your application. Instead, it walks over the existing HTML tree, “picking up” existing elements and adding the necessary interactivity.
> Note that there are some trade-offs here. Before this hydration process is complete, the page will _appear_ interactive but wont actually respond to interactions. For example, if you have a counter button and click it before WASM has loaded, the count will not increment, because the necessary event listeners and reactivity have not been added yet. Well look at some ways to build in “graceful degradation” in future chapters.
## Client-Side Navigation
The next step is very important. Imagine that the user now clicks a link to navigate to another page in your application.
The browser will _not_ make another round trip to the server, reloading the full page as it would for navigating between plain HTML pages or an application that uses server rendering (for example with PHP) but without a client-side half.
Instead, the WASM version of your app will load the new page, right there in the browser, without requesting another page from the server. Essentially, your app upgrades itself from a server-loaded “multi-page app” into a browser-rendered “single-page app.” This yields the best of both worlds: a fast initial load time due to the server-rendered HTML, and fast secondary navigations because of the client-side routing.
Some of what will be described in the following chapters—like the interactions between server functions, resources, and `<Suspense/>`—may seem overly complicated. You might find yourself asking, “If my page is being rendered to HTML on the server, why cant I just `.await` this on the server? If I can just call library X in a server function, why cant I call it in my component?” The reason is pretty simple: to enable the upgrade from server rendering to client rendering, everything in your application must be able to run either on the server or in the browser.
This is not the only way to create a website or web framework, of course. But its the most common way, and we happen to think its quite a good way, to create the smoothest possible experience for your users.

View File

@@ -0,0 +1,122 @@
# Async Rendering and SSR “Modes”
Server-rendering a page that uses only synchronous data is pretty simple: You just walk down the component tree, rendering each element to an HTML string. But this is a pretty big caveat: it doesnt answer the question of what we should do with pages that includes asynchronous data, i.e., the sort of stuff that would be rendered under a `<Suspense/>` node on the client.
When a page loads async data that it needs to render, what should we do? Should we wait for all the async data to load, and then render everything at once? (Lets call this “async” rendering) Should we go all the way in the opposite direction, just sending the HTML we have immediately down to the client and letting the client load the resources and fill them in? (Lets call this “synchronous” rendering) Or is there some middle-ground solution that somehow beats them both? (Hint: There is.)
If youve ever listened to streaming music or watched a video online, Im sure you realize that HTTP supports streaming, allowing a single connection to send chunks of data one after another without waiting for the full content to load. You may not realize that browsers are also really good at rendering partial HTML pages. Taken together, this means that you can actually enhance your users experience by **streaming HTML**: and this is something that Leptos supports out of the box, with no configuration at all. And theres actually more than one way to stream HTML: you can stream the chunks of HTML that make up your page in order, like frames of a video, or you can stream them... well, out of order.
Let me say a little more about what I mean.
Leptos supports all four different of these different ways to render HTML that includes asynchronous data.
## Synchronous Rendering
1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `<Suspense/>`. Load data on the client using `create_local_resource`, replacing `fallback` once resources are loaded.
- _Pros_: App shell appears very quickly: great TTFB (time to first byte).
- _Cons_
- Resources load relatively slowly; you need to wait for JS + WASM to load before even making a request.
- No ability to include data from async resources in the `<title>` or other `<meta>` tags, hurting SEO and things like social media link previews.
If youre using server-side rendering, the synchronous mode is almost never what you actually want, from a performance perspective. This is because it misses out on an important optimization. If youre loading async resources during server rendering, you can actually begin loading the data on the server. Rather than waiting for the client to receive the HTML response, then loading its JS + WASM, _then_ realize it needs the resources and begin loading them, server rendering can actually begin loading the resources when the client first makes the response. In this sense, during server rendering an async resource is like a `Future` that begins loading on the server and resolves on the client. As long as the resources are actually serializable, this will always lead to a faster total load time.
> This is why [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) requires resources data to be serializable by default, and why you need to explicitly use [`create_local_resource`](https://docs.rs/leptos/latest/leptos/fn.create_local_resource.html) for any async data that is not serializable and should therefore only be loaded in the browser itself. Creating a local resource when you could create a serializable resource is always a deoptimization.
## Async Rendering
<video controls>
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/async.mov?raw=true" type="video/mp4">
</video>
2. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
- _Pros_: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
- _Cons_: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client. The page is totally blank until everything is loaded.
## In-Order Streaming
<video controls>
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/in-order.mov?raw=true" type="video/mp4">
</video>
3. **In-order streaming**: Walk through the component tree, rendering HTML until you hit a `<Suspense/>`. Send down all the HTML youve got so far as a chunk in the stream, wait for all the resources accessed under the `<Suspense/>` to load, then render it to HTML and keep walking until you hit another `<Suspense/>` or the end of the page.
- _Pros_: Rather than a blank screen, shows at least _something_ before the data are ready.
- _Cons_
- Loads the shell more slowly than synchronous rendering (or out-of-order streaming) because it needs to pause at every `<Suspense/>`.
- Unable to show fallback states for `<Suspense/>`.
- Cant begin hydration until the entire page has loaded, so earlier pieces of the page will not be interactive until the suspended chunks have loaded.
## Out-of-Order Streaming
<video controls>
<source src="https://github.com/leptos-rs/leptos/blob/main/docs/video/out-of-order.mov?raw=true" type="video/mp4">
</video>
4. **Out-of-order streaming**: Like synchronous rendering, serve an HTML shell that includes `fallback` for any `<Suspense/>`. But load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `<Suspense/>` nodes, which is swapped in to replace the fallback.
- _Pros_: Combines the best of **synchronous** and **`async`**.
- Fast initial response/TTFB because it immediately sends the whole synchronous shell
- Fast total time because resources begin loading on the server.
- Able to show the fallback loading state and dynamically replace it, instead of showing blank sections for un-loaded data.
- _Cons_: Requires JavaScript to be enabled for suspended fragments to appear in correct order. (This small chunk of JS streamed down in a `<script>` tag alongside the `<template>` tag that contains the rendered `<Suspense/>` fragment, so it does not need to load any additional JS files.)
## Using SSR Modes
Because it offers the best blend of performance characteristics, Leptos defaults to out-of-order streaming. But its really simple to opt into these different modes. You do it by adding an `ssr` property onto one or more of your `<Route/>` components, like in the [`ssr_modes` example](https://github.com/leptos-rs/leptos/blob/main/examples/ssr_modes/src/app.rs).
```rust
<Routes>
// Well load the home page with out-of-order streaming and <Suspense/>
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
// We'll load the posts with async rendering, so they can set
// the title and metadata *after* loading the data
<Route
path="/post/:id"
view=|cx| view! { cx, <Post/> }
ssr=SsrMode::Async
/>
</Routes>
```
For a path that includes multiple nested routes, the most restrictive mode will be used: i.e., if even a single nested route asks for `async` rendering, the whole initial request will be rendered `async`. `async` is the most restricted requirement, followed by in-order, and then out-of-order. (This probably makes sense if you think about it for a few minutes.)
## Blocking Resources
Any Leptos versions later than `0.2.5` (i.e., git main and `0.3.x` or later) introduce a new resource primitive with `create_blocking_resource`. A blocking resource still loads asynchronously like any other `async`/`.await` in Rust; it doesnt block a server thread or anything. Instead, reading from a blocking resource under a `<Suspense/>` blocks the HTML _stream_ from returning anything, including its initial synchronous shell, until that `<Suspense/>` has resolved.
Now from a performance perspective, this is not ideal. None of the synchronous shell for your page will load until that resource is ready. However, rendering nothing means that you can do things like set the `<title>` or `<meta>` tags in your `<head>` in actual HTML. This sounds a lot like `async` rendering, but theres one big difference: if you have multiple `<Suspense/>` sections, you can block on _one_ of them but still render a placeholder and then stream in the other.
For example, think about a blog post. For SEO and for social sharing, I definitely want my blog posts title and metadata in the initial HTML `<head>`. But I really dont care whether comments have loaded yet or not; Id like to load those as lazily as possible.
With blocking resources, I can do something like this:
```rust
#[component]
pub fn BlogPost(cx: Scope) -> impl IntoView {
let post_data = create_blocking_resource(cx, /* load blog post */);
let comment_data = create_resource(cx, /* load blog post */);
view! { cx,
<Suspense fallback=|| ()>
{move || {
post_data.with(cx, |data| {
view! { cx,
<Title text=data.title/>
<Meta name="description" content=data.excerpt/>
<article>
/* render the post content */
</article>
}
})
}}
</Suspense>
<Suspense fallback=|| "Loading comments...">
/* render comment data here */
</Suspense>
}
}
```
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.

View File

@@ -0,0 +1,148 @@
# Hydration Bugs _(and how to avoid them)_
## A Thought Experiment
Lets try an experiment to test your intuitions. Open up an app youre server-rendering with `cargo-leptos`. (If youve just been using `trunk` so far to play with examples, go [clone a `cargo-leptos` template](./21_cargo_leptos.md) just for the sake of this exercise.)
Put a log somewhere in your root component. (I usually call mine `<App/>`, but anything will do.)
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
leptos::log!("where do I run?");
// ... whatever
}
```
And lets fire it up
```bash
cargo leptos watch
```
Where do you expect `where do I run?` to log?
- In the command line where youre running the server?
- In the browser console when you load the page?
- Neither?
- Both?
Try it out.
...
...
...
Okay, consider the spoiler alerted.
Youll notice of course that it logs in both places, assuming everything goes according to plan. In fact on the server it logs twice—first during the initial server startup, when Leptos renders your app once to extract the route tree, then a second time when you make a request. Each time you reload the page, `where do I run?` should log once on the server and once on the client.
If you think about the description in the last couple sections, hopefully this makes sense. Your application runs once on the server, where it builds up a tree of HTML which is sent to the client. During this initial render, `where do I run?` logs on the server.
Once the WASM binary has loaded in the browser, your application runs a second time, walking over the same user interface tree and adding interactivity.
> Does that sound like a waste? It is, in a sense. But reducing that waste is a genuinely hard problem. Its what some JS frameworks like Qwik are intended to solve, although its probably too early to tell whether its a net performance gain as opposed to other approaches.
## The Potential for Bugs
Okay, hopefully all of that made sense. But what does it have to do with the title of this chapter, which is “Hydration bugs (and how to avoid them)”?
Remember that the application needs to run on both the server and the client. This generates a few different sets of potential issues you need to know how to avoid.
### Mismatches between server and client code
One way to create a bug is by creating a mismatch between the HTML thats sent down by the server and whats rendered on the client. Its actually fairly hard to do this unintentionally, I think (at least judging by the bug reports I get from people.) But imagine I do something like this
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let data = if cfg!(target_arch = "wasm32") {
vec![0, 1, 2]
} else {
vec![]
};
data.into_iter()
.map(|value| view! { cx, <span>{value}</span> })
.collect::<Vec<_>>()
}
```
In other words, if this is being compiled to WASM, it has three items; otherwise its empty.
When I load the page in the browser, I see nothing. If I open the console I see a bunch of warnings:
```
element with id 0-0-1 not found, ignoring it for hydration
element with id 0-0-2 not found, ignoring it for hydration
element with id 0-0-3 not found, ignoring it for hydration
component with id _0-0-4c not found, ignoring it for hydration
component with id _0-0-4o not found, ignoring it for hydration
```
The WASM version of your app, running in the browser, expects to find three items; but the HTML has none.
#### Solution
Its pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If youre seeing warnings like this and you dont think its your fault, its much more likely that its a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
### Not all client code can run on the server
Imagine you happily import a dependency like `gloo-net` that youve been used to using to make requests in the browser, and use it in a `create_resource` in a server-rendered app.
Youll probably instantly see the dreaded message
```
panicked at 'cannot call wasm-bindgen imported functions on non-wasm targets'
```
Uh-oh.
But of course this makes sense. Weve just said that your app needs to run on the client and the server.
#### Solution
There are a few ways to avoid this:
1. Only use libraries that can run on both the server and the client. `reqwest`, for example, works for making HTTP requests in both settings.
2. Use different libraries on the server and the client, and gate them using the `#[cfg]` macro. ([Click here for an example](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).)
3. Wrap client-only code in `create_effect`. Because `create_effect` only runs on the client, this can be an effective way to access browser APIs that are not needed for initial rendering.
For example, say that I want to store something in the browsers `localStorage` whenever a signal changes.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
use gloo_storage::Storage;
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
}
```
This panics because I cant access `LocalStorage` during server rendering.
But if I wrap it in an effect...
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
use gloo_storage::Storage;
create_effect(cx, move |_| {
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
});
}
```
Its fine! This will render appropriately on the server, ignoring the client-only code, and then access the storage and log a message on the browser.
### Not all server code can run on the client
WebAssembly running in the browser is a pretty limited environment. You dont have access to a file-system or to many of the other things the standard library may be used to having. Not every crate can even be compiled to WASM, let alone run in a WASM environment.
In particular, youll sometimes see errors about the crate `mio` or missing things from `core`. This is generally a sign that you are trying to compile something to WASM that cant be compiled to WASM. If youre adding server-only dependencies, youll want to mark them `optional = true` in your `Cargo.toml` and then enable them in the `ssr` feature definition. (Check out one of the template `Cargo.toml` files to see more details.)
You can use `create_effect` to specify that something should only run on the client, and not in the server. Is there a way to specify that something should run only on the server, and not the client?
In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html).)

View File

@@ -0,0 +1,21 @@
# Server Side Rendering
So far, everything weve written has been rendered almost entirely in the browser. When we create an app using Trunk, its served using a local development server. If you build it for production and deploy it, its served by whatever server or CDN youre using. In either case, whats served is an HTML page with
1. the URL of your Leptos app, which has been compiled to WebAssembly (WASM)
2. the URL of the JavaScript used to initialized this WASM blob
3. an empty `<body>` element
When the JS and WASM have loaded, Leptos will render your app into the `<body>`. This means that nothing appears on the screen until JS/WASM have loaded and run. This has some drawbacks:
1. It increases load time, as your users screen is blank until additional resources have been downloaded.
2. Its bad for SEO, as load times are longer and the HTML you serve has no meaningful content.
3. Its broken for users for whom JS/WASM dont load for some reason (e.g., theyre on a train and just went into a tunnel before WASM finished loading; theyre using an older device that doesnt support WASM; they have JavaScript or WASM turned off for some reason; etc.)
These downsides apply across the web ecosystem, but especially to WASM apps.
So what do you do if you want to return more than just an empty `<body>` tag? Use “server-side rendering.”
Whole books could be (and probably have been) written about this topic, but at its core, its really simple: rather than returning an empty `<body>` tag, return an initial HTML page that reflects the actual starting state of your app or site, so that while JS/WASM are loading, and until they load, the user can access the plain HTML version.
The rest of this section will cover this topic in some detail!

View File

@@ -15,7 +15,7 @@ covered some of this in the material on [components and props](./03_components.m
Basically if you want the parent to communicate to the child, you can pass a
[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/struct.MaybeSignal.html) as a prop.
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) as a prop.
But what about the other direction? How can a child send notifications about events
or state changes back up to the parent?

BIN
docs/video/async.mov Normal file

Binary file not shown.

BIN
docs/video/in-order.mov Normal file

Binary file not shown.

BIN
docs/video/out-of-order.mov Normal file

Binary file not shown.

61
examples/Makefile.toml Normal file
View File

@@ -0,0 +1,61 @@
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
# Emulate workspace
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
"counters",
"counters_stable",
"counter_without_macros",
"error_boundary",
"errors_axum",
"fetch",
"hackernews",
"hackernews_axum",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"ssr_modes",
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
[tasks.verify]
description = "Run all quality checks and tests"
dependencies = ["check-style", "test-unit-and-web"]
[tasks.test-unit-and-web]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.pre-verify-flow]
[tasks.post-verify-flow]
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
[tasks.pre-web-test-flow]
[tasks.web-test]
[tasks.post-web-test-flow]

View File

@@ -3,6 +3,10 @@ name = "counter"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "1"
@@ -12,5 +16,4 @@ console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys ="0.3"
web-sys = "0.3"

View File

@@ -6,6 +6,10 @@ edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }

View File

@@ -3,6 +3,10 @@ name = "counter_without_macros"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
console_log = "1"
@@ -10,4 +14,10 @@ log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.34"
pretty_assertions = "1.3.0"
[dev-dependencies.web-sys]
features = ["HtmlElement", "XPathResult"]
version = "0.3.61"

View File

@@ -1,3 +1,10 @@
[env]
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
[tasks.web-test]
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.build]
command = "cargo"
args = ["+stable", "build-all-features"]

View File

@@ -3,3 +3,5 @@
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
Issue the `cargo make test-flow` command to run unit and wasm tests.

View File

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

View File

@@ -0,0 +1,86 @@
use counter_without_macros::counter;
use leptos::*;
use pretty_assertions::assert_eq;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::HtmlElement;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn should_increment_counter() {
open_counter();
click_increment();
click_increment();
assert_eq!(see_text(), Some("Value: 2!".to_string()));
}
#[wasm_bindgen_test]
fn should_decrement_counter() {
open_counter();
click_decrement();
click_decrement();
assert_eq!(see_text(), Some("Value: -2!".to_string()));
}
#[wasm_bindgen_test]
fn should_clear_counter() {
open_counter();
click_increment();
click_increment();
click_clear();
assert_eq!(see_text(), Some("Value: 0!".to_string()));
}
fn open_counter() {
remove_existing_counter();
mount_to_body(move |cx| counter(cx, 0, 1));
}
fn remove_existing_counter() {
if let Some(counter) =
leptos::document().query_selector("body div").unwrap()
{
counter.remove();
}
}
fn click_clear() {
click_text("Clear");
}
fn click_decrement() {
click_text("-1");
}
fn click_increment() {
click_text("+1");
}
fn click_text(text: &str) {
find_by_text(text).click();
}
fn see_text() -> Option<String> {
find_by_text("Value: ").text_content()
}
fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = leptos::document();
document
.evaluate(&xpath, &document)
.unwrap()
.iterate_next()
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
}

View File

@@ -11,4 +11,5 @@ console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2"
web-sys = "0.3"

View File

@@ -38,7 +38,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
};
view! { cx,
<>
<div>
<button on:click=add_counter>
"Add Counter"
</button>
@@ -72,7 +72,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
}
/>
</ul>
</>
</div>
}
}

View File

@@ -1,10 +1,11 @@
use wasm_bindgen_test::*;
use wasm_bindgen::JsCast;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::*;
use web_sys::HtmlElement;
use counters::{Counters, CountersProps};
use counters::Counters;
#[wasm_bindgen_test]
fn inc() {
@@ -24,7 +25,7 @@ fn inc() {
add_counter.click();
// check HTML
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>0</span> from <span>3</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li></ul>");
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->0<!-- </DynChild> --></span> from <span><!-- <DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
let counters = div
.query_selector("ul")
@@ -52,7 +53,7 @@ fn inc() {
}
}
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>6</span> from <span>3</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>1</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>2</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>3</span><button>+1</button><button>x</button></li></ul>");
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->6<!-- </DynChild> --></span> from <span><!-- <DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->1<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->2<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->3<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
// remove the first counter
counters
@@ -63,51 +64,5 @@ fn inc() {
.unchecked_into::<HtmlElement>()
.click();
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>5</span> from <span>2</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>2</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>3</span><button>+1</button><button>x</button></li></ul>");
// decrement all by 1
for idx in 0..counters.length() {
let counter = counters.item(idx).unwrap();
let dec_button = counter
.first_child()
.unwrap()
.unchecked_into::<HtmlElement>();
dec_button.click();
}
run_scope(create_runtime(), move |cx| {
// we can use RSX in test comparisons!
// note that if RSX template creation is bugged, this probably won't catch it
// (because the same bug will be reproduced in both sides of the assertion)
// so I use HTML tests for most internal testing like this
// but in user-land testing, RSX comparanda are cool
assert_eq!(
div.outer_html(),
view! { cx,
<div>
<button>"Add Counter"</button>
<button>"Add 1000 Counters"</button>
<button>"Clear Counters"</button>
<p>"Total: "<span>"3"</span>" from "<span>"2"</span>" counters."</p>
<ul>
<li>
<button>"-1"</button>
<input type="text"/>
<span>"1"</span>
<button>"+1"</button>
<button>"x"</button>
</li>
<li>
<button>"-1"</button>
<input type="text"/>
<span>"2"</span>
<button>"+1"</button>
<button>"x"</button>
</li>
</ul>
</div>
}
.outer_html()
);
});
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->5<!-- </DynChild> --></span> from <span><!-- <DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->2<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->3<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
}

View File

@@ -3,6 +3,10 @@ name = "error_boundary"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "1"

View File

@@ -28,8 +28,6 @@ thiserror = "1.0.38"
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
@@ -44,7 +42,7 @@ ssr = [
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name

View File

@@ -3,15 +3,18 @@ name = "fetch"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
anyhow = "1.0.58"
leptos = { path = "../../leptos" }
reqwasm = "0.5.0"
reqwasm = "0.5"
serde = { version = "1", features = ["derive"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
console_error_panic_hook = "0.1"
thiserror = "1"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen-test = "0.3"

View File

@@ -1,38 +1,50 @@
use anyhow::Result;
use leptos::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cat {
url: String,
}
async fn fetch_cats(count: u32) -> Result<Vec<String>> {
#[derive(Error, Clone, Debug)]
pub enum FetchError {
#[error("Please request more than zero cats.")]
NonZeroCats,
#[error("Error loading data from serving.")]
Request,
#[error("Error deserializaing cat data from request.")]
Json
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
if count > 0 {
// make the request
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={count}",
))
.send()
.await?
.await
.map_err(|_| FetchError::Request)?
// convert it to JSON
.json::<Vec<Cat>>()
.await?
.await
.map_err(|_| FetchError::Json)?
// extract the URL field for each cat
.into_iter()
.map(|cat| cat.url)
.collect::<Vec<_>>();
Ok(res)
} else {
Ok(vec![])
Err(FetchError::NonZeroCats)
}
}
pub fn fetch_example(cx: Scope) -> impl IntoView {
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 0);
// we use local_resource here because
// 1) anyhow::Result isn't serializable/deserializable
// 1) our error type isn't serializable/deserializable
// 2) we're not doing server-side rendering in this example anyway
// (during SSR, create_resource will begin loading on the server and resolve on the client)
let cats = create_local_resource(cx, cat_count, fetch_cats);
@@ -42,7 +54,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
errors.with(|errors| {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
.collect::<Vec<_>>()
})
};
@@ -60,11 +72,12 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just implement our happy path and let the framework handle the rest
let cats_view = move || {
cats.with(cx, |data| {
data.iter()
.flatten()
.map(|cat| view! { cx, <img src={cat}/> })
.collect::<Vec<_>>()
cats.read(cx).map(|data| {
data.map(|data| {
data.iter()
.map(|s| view! { cx, <span>{s}</span> })
.collect::<Vec<_>>()
})
})
};
@@ -72,8 +85,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
<div>
<label>
"How many cats would you like?"
<input type="number"
prop:value={move || cat_count.get().to_string()}
<input
type="number"
prop:value=move || cat_count.get().to_string()
on:input=move |ev| {
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
set_cat_count(val);
@@ -81,7 +95,9 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
/>
</label>
<ErrorBoundary fallback>
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
<Transition fallback=move || {
view! { cx, <div>"Loading (Suspense Fallback)..."</div> }
}>
{cats_view}
</Transition>
</ErrorBoundary>

View File

@@ -6,6 +6,10 @@ edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }

View File

@@ -6,6 +6,10 @@ edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"

View File

@@ -1,6 +1,10 @@
[workspace]
members = ["client", "api-boundary", "server"]
[profile.release]
codegen-units = 1
lto = true
[patch.crates-io]
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router" }

View File

@@ -3,6 +3,10 @@ name = "parent-child"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "1"

View File

@@ -3,6 +3,10 @@ name = "router"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1"
log = "0.4"

View File

@@ -3,17 +3,7 @@
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<style>
a[aria-current] {
font-weight: bold;
}
.contact, .contact-list {
border: 1px solid #c0c0c0;
border-radius: 3px;
padding: 1rem;
}
</style>
<link data-trunk rel="css" href="style.css"/>
</head>
<body></body>
</html>
</html>

View File

@@ -27,7 +27,12 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
<A href="redirect-home">"Redirect to Home"</A>
</nav>
<main>
<Routes>
<AnimatedRoutes
outro="slideOut"
intro="slideIn"
outro_back="slideOutBack"
intro_back="slideInBack"
>
<ContactRoutes/>
<Route
path="about"
@@ -41,7 +46,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
path="redirect-home"
view=move |cx| view! { cx, <Redirect path="/"/> }
/>
</Routes>
</AnimatedRoutes>
</main>
</Router>
}
@@ -102,7 +107,11 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
{move || view! { cx, <ul>{contacts}</ul>}}
</Suspense>
<Outlet/>
<AnimatedOutlet
class="outlet"
outro="fadeOut"
intro="fadeIn"
/>
</div>
}
}

95
examples/router/style.css Normal file
View File

@@ -0,0 +1,95 @@
a[aria-current] {
font-weight: bold;
}
.outlet {
border: 1px dotted grey;
}
.contact, .contact-list {
border: 1px solid #c0c0c0;
border-radius: 3px;
padding: 1rem;
}
.fadeIn {
animation: 0.5s fadeIn forwards;
}
.fadeOut {
animation: 0.5s fadeOut forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.slideIn {
animation: 0.25s slideIn forwards;
}
.slideOut {
animation: 0.25s slideOut forwards;
}
@keyframes slideIn {
from {
transform: translate(100vw, 0);
}
to {
transform: translate(0px, 0px);
}
}
@keyframes slideOut {
from {
transform: translate(0px, 0px);
}
to {
transform: translate(-100vw, 0);
}
}
.slideInBack {
animation: 0.25s slideInBack forwards;
}
.slideOutBack {
animation: 0.25s slideOutBack forwards;
}
@keyframes slideInBack {
from {
transform: translate(-100vw, 0);
}
to {
transform: translate(0px, 0px);
}
}
@keyframes slideOutBack {
from {
transform: translate(0px, 0px);
}
to {
transform: translate(100vw, 0);
}
}

View File

@@ -43,8 +43,6 @@ bcrypt = { version = "0.14", optional = true }
async-trait = { version = "0.1.64", optional = true }
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
@@ -65,7 +63,7 @@ ssr = [
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name

View File

@@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@@ -16,12 +19,15 @@
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@@ -61,11 +67,11 @@
]
},
"locked": {
"lastModified": 1677292251,
"narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=",
"lastModified": 1681525152,
"narHash": "sha256-KzI+ILcmU03iFWtB+ysPqtNmp8TP8v1BBReTuPP8MJY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc",
"rev": "b6f8d87208336d7cb85003b2e439fc707c38f92a",
"type": "github"
},
"original": {
@@ -73,6 +79,36 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

14
examples/slots/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "slots"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

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

7
examples/slots/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Leptos `<Component slot/>` Example
This example shows how to use Slots in Leptos.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

63
examples/slots/src/lib.rs Normal file
View File

@@ -0,0 +1,63 @@
use leptos::*;
// Slots are created in simillar manner to components, except that they use the #[slot] macro.
#[slot]
struct Then {
children: ChildrenFn,
}
// Props work just like component props, for example, you can specify a prop as optional by prefixing
// the type with Option<...> and marking the option as #[prop(optional)].
#[slot]
struct ElseIf {
cond: MaybeSignal<bool>,
children: ChildrenFn,
}
#[slot]
struct Fallback {
children: ChildrenFn,
}
// Slots are added to components like any other prop.
#[component]
fn SlotIf(
cx: Scope,
cond: MaybeSignal<bool>,
then: Then,
#[prop(default=vec![])] else_if: Vec<ElseIf>,
#[prop(optional)] fallback: Option<Fallback>,
) -> impl IntoView {
move || {
if cond() {
(then.children)(cx).into_view(cx)
} else if let Some(else_if) = else_if.iter().find(|i| (i.cond)()) {
(else_if.children)(cx).into_view(cx)
} else if let Some(fallback) = &fallback {
(fallback.children)(cx).into_view(cx)
} else {
().into_view(cx)
}
}
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
let is_even = MaybeSignal::derive(cx, move || count() % 2 == 0);
let is_div5 = MaybeSignal::derive(cx, move || count() % 5 == 0);
let is_div7 = MaybeSignal::derive(cx, move || count() % 7 == 0);
view! { cx,
<button on:click=move |_| set_count.update(|value| *value += 1)>"+1"</button>
" "{count}" is "
<SlotIf cond=is_even>
// The slot name can be emitted if it would match the slot struct name (in snake case).
<Then slot>"even"</Then>
// Props are passed just like on normal components.
<ElseIf slot cond=is_div5>"divisible by 5"</ElseIf>
<ElseIf slot cond=is_div7>"divisible by 7"</ElseIf>
<Fallback slot>"odd"</Fallback>
</SlotIf>
}
}

View File

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

View File

@@ -36,6 +36,10 @@ ssr = [
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"

View File

@@ -39,6 +39,10 @@ ssr = [
"dep:leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "ssr_modes"

View File

@@ -1,19 +1,23 @@
# Leptos Todo App Sqlite
# Leptos Todo App Sqlite
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
@@ -21,24 +25,30 @@ cargo leptos watch
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

View File

@@ -156,11 +156,11 @@ pub fn Todos(cx: Scope) -> impl IntoView {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
@@ -175,9 +175,9 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</ActionForm>
</li>
}
.into_any()
})
.collect::<Vec<_>>()
.into_view(cx)
}
}
})

View File

@@ -18,6 +18,7 @@ cfg_if! {
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
_ = FormDataHandler::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@@ -106,6 +107,24 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct FormData {
hi: String
}
#[server(FormDataHandler, "/api")]
pub async fn form_data(cx: Scope) -> Result<FormData, ServerFnError> {
use axum::extract::FromRequest;
let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx).and_then(|req| req.take_request()).unwrap();
if req.method() == http::Method::POST {
let form = axum::Form::from_request(req, &()).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(form.0)
} else {
Err(ServerFnError::ServerError("wrong form fields submitted".to_string()))
}
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
//let id = use_context::<String>(cx);
@@ -126,6 +145,23 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
<Todos/>
</ErrorBoundary>
}/> //Route
<Route path="weird" methods=&[Method::Get, Method::Post]
ssr=SsrMode::Async
view=|cx| {
let res = create_resource(cx, || (), move |_| async move {
form_data(cx).await
});
view! { cx,
<Suspense fallback=|| ()>
<pre>
{move || {
res.with(cx, |body| format!("{body:#?}"))
}}
</pre>
</Suspense>
}
}
/>
</Routes>
</main>
</Router>
@@ -147,6 +183,10 @@ pub fn Todos(cx: Scope) -> impl IntoView {
view! {
cx,
<form method="POST" action="/weird">
<input type="text" name="hi" value="John"/>
<input type="submit"/>
</form>
<div>
<MultiActionForm action=add_todo>
<label>
@@ -162,11 +202,11 @@ pub fn Todos(cx: Scope) -> impl IntoView {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
@@ -181,9 +221,9 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</ActionForm>
</li>
}
.into_any()
})
.collect::<Vec<_>>()
.into_view(cx)
}
}
})

View File

@@ -163,11 +163,11 @@ pub fn Todos(cx: Scope) -> impl IntoView {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
@@ -182,9 +182,9 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</ActionForm>
</li>
}
.into_any()
})
.collect::<Vec<_>>()
.into_view(cx)
}
}
})

View File

@@ -3,6 +3,10 @@ name = "todomvc"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", default-features = false }
log = "0.4"
@@ -21,3 +25,6 @@ default = ["csr"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
[package.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

130
flake.lock generated Normal file
View File

@@ -0,0 +1,130 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1681920287,
"narHash": "sha256-+/d6XQQfhhXVfqfLROJoqj3TuG38CAeoT6jO1g9r1k0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "645bc49f34fa8eff95479f0345ff57e55b53437e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1682043560,
"narHash": "sha256-ZsF4Yee9pQbvLtwSVGgYux+az4yFSLXrxPyGHm3ptJM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "48037a6f8faeee138ede96bf607bc95c9dab9aec",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

36
flake.nix Normal file
View File

@@ -0,0 +1,36 @@
{
description = "A basic Rust devshell for NixOS users developing Leptos";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
in
with pkgs;
{
devShells.default = mkShell {
buildInputs = [
openssl
pkg-config
cacert
(rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override {
extensions= [ "rust-src" "rust-analyzer" ];
targets = [ "wasm32-unknown-unknown" ];
}))
];
shellHook = ''
'';
};
}
);
}

View File

@@ -17,3 +17,4 @@ leptos_integration_utils = { workspace = true }
serde_json = "1"
parking_lot = "0.12.1"
regex = "1.7.0"
tracing = "0.1.37"

View File

@@ -27,7 +27,7 @@ use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::sync::Arc;
use tracing::instrument;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
#[derive(Debug, Clone, Default)]
@@ -98,6 +98,7 @@ impl ResponseOptions {
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn redirect(cx: leptos::Scope, path: &str) {
if let Some(response_options) = use_context::<ResponseOptions>(cx) {
response_options.set_status(StatusCode::FOUND);
@@ -147,6 +148,7 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|_cx| {})
}
@@ -166,6 +168,7 @@ pub fn handle_server_fns() -> Route {
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn handle_server_fns_with_context(
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> Route {
@@ -201,15 +204,11 @@ pub fn handle_server_fns_with_context(
Encoding::Url | Encoding::Cbor => body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
match (server_fn.trait_obj)(cx, data).await {
let res = match (server_fn.trait_obj)(cx, data).await {
Ok(serialized) => {
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
let mut res: HttpResponseBuilder;
let mut res_parts = res_options.0.write();
@@ -268,7 +267,11 @@ pub fn handle_server_fns_with_context(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
),
}
};
// clean up the scope
disposer.dispose();
runtime.dispose();
res
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
@@ -298,6 +301,7 @@ pub fn handle_server_fns_with_context(
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -321,6 +325,7 @@ pub fn handle_server_fns_with_context(
/// leptos_actix::render_app_to_stream(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// Method::Get,
/// ),
/// )
/// })
@@ -337,14 +342,16 @@ pub fn handle_server_fns_with_context(
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_with_context(options, |_cx| {}, app_fn)
render_app_to_stream_with_context(options, |_cx| {}, app_fn, method)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -363,6 +370,7 @@ where
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -386,6 +394,7 @@ where
/// leptos_actix::render_app_to_stream_in_order(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// Method::Get,
/// ),
/// )
/// })
@@ -402,14 +411,21 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream_in_order<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_in_order_with_context(options, |_cx| {}, app_fn)
render_app_to_stream_in_order_with_context(
options,
|_cx| {},
app_fn,
method,
)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -426,6 +442,7 @@ where
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// use leptos_router::Method;
/// use std::{env, net::SocketAddr};
///
/// #[component]
@@ -449,6 +466,7 @@ where
/// leptos_actix::render_app_async(
/// leptos_options.to_owned(),
/// |cx| view! { cx, <MyApp/> },
/// Method::Get,
/// ),
/// )
/// })
@@ -465,14 +483,16 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_async<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_async_with_context(options, |_cx| {}, app_fn)
render_app_async_with_context(options, |_cx| {}, app_fn, method)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -487,15 +507,17 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
@@ -513,7 +535,14 @@ where
stream_app(&options, app, res_options, additional_context).await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -528,15 +557,17 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream_in_order_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
@@ -555,7 +586,14 @@ where
stream_app_in_order(&options, app, res_options, additional_context)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -571,15 +609,17 @@ where
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_async_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let handler = move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
@@ -603,7 +643,14 @@ where
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
@@ -703,7 +750,7 @@ where
}
})
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn provide_contexts(
cx: leptos::Scope,
req: &HttpRequest,
@@ -728,7 +775,7 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
"http://leptos".to_string() + path + "?" + query
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn stream_app(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
@@ -744,7 +791,10 @@ async fn stream_app(
build_stream_response(options, res_options, stream, runtime, scope).await
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
async fn stream_app_in_order(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
@@ -762,7 +812,7 @@ async fn stream_app_in_order(
build_stream_response(options, res_options, stream, runtime, scope).await
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn build_stream_response(
options: &LeptosOptions,
res_options: ResponseOptions,
@@ -812,7 +862,7 @@ async fn build_stream_response(
// Return the response
res
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn render_app_async_helper(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
@@ -854,7 +904,7 @@ async fn render_app_async_helper(
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
) -> Vec<(String, SsrMode)>
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -863,11 +913,16 @@ where
// Empty strings screw with Actix pathing, they need to be "/"
routes = routes
.into_iter()
.map(|(s, mode)| {
if s.is_empty() {
return ("/".to_string(), mode);
.map(|listing| {
let path = listing.path();
if path.is_empty() {
return RouteListing::new(
"/".to_string(),
listing.mode(),
listing.methods(),
);
}
(s, mode)
RouteListing::new(listing.path(), listing.mode(), listing.methods())
})
.collect();
@@ -877,14 +932,19 @@ where
// Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}`
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
let routes: Vec<(String, SsrMode)> = routes
let routes = routes
.into_iter()
.map(|(s, m)| (wildcard_re.replace_all(&s, "{tail:.*}").to_string(), m))
.map(|(s, m)| (capture_re.replace_all(&s, "{$1}").to_string(), m))
.collect();
.map(|listing| {
let path = wildcard_re
.replace_all(listing.path(), "{tail:.*}")
.to_string();
let path = capture_re.replace_all(&path, "{$1}").to_string();
RouteListing::new(path, listing.mode(), listing.methods())
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![("/".to_string(), Default::default())]
vec![RouteListing::new("/", Default::default(), [Method::Get])]
} else {
routes
}
@@ -901,7 +961,7 @@ pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -926,7 +986,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -945,10 +1005,11 @@ where
InitError = (),
>,
{
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -956,7 +1017,7 @@ where
{
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
options: LeptosOptions,
@@ -984,11 +1045,11 @@ where
}
router
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -996,29 +1057,39 @@ where
IV: IntoView + 'static,
{
let mut router = self;
for (path, mode) in paths.iter() {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
for listing in paths.iter() {
let path = listing.path();
let mode = listing.mode();
for method in listing.methods() {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)
}
SsrMode::Async => render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
},
);
method,
),
},
);
}
}
router
}

View File

@@ -19,4 +19,6 @@ leptos_integration_utils = { workspace = true }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"
tokio-util = {version = "0.7.7", features = ["rt"] }
tokio-util = {version = "0.7.7", features = ["rt"] }
tracing = "0.1.37"
once_cell = "1.17"

View File

@@ -1,5 +1,4 @@
#![forbid(unsafe_code)]
//! Provides functions to easily integrate Leptos with Axum.
//!
//! For more details on how to use the integrations, see the
@@ -14,7 +13,7 @@ use axum::{
HeaderMap, Request, StatusCode,
},
response::IntoResponse,
routing::get,
routing::{delete, get, patch, post, put},
};
use futures::{
channel::mpsc::{Receiver, Sender},
@@ -31,16 +30,12 @@ use leptos::{
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_router::*;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::{
io,
pin::Pin,
sync::{Arc, OnceLock},
thread::available_parallelism,
};
use std::{io, pin::Pin, sync::Arc, thread::available_parallelism};
use tokio::task::LocalSet;
use tokio_util::task::LocalPoolHandle;
use tracing::Instrument;
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
/// to construct this for Leptos to use in Axum
#[derive(Debug, Clone)]
@@ -252,6 +247,7 @@ where
/// This function always provides context values including the following types:
/// - [RequestParts]
/// - [ResponseOptions]
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn handle_server_fns(
Path(fn_name): Path<String>,
headers: HeaderMap,
@@ -275,6 +271,7 @@ pub async fn handle_server_fns(
/// This function always provides context values including the following types:
/// - [RequestParts]
/// - [ResponseOptions]
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn handle_server_fns_with_context(
Path(fn_name): Path<String>,
headers: HeaderMap,
@@ -285,7 +282,7 @@ pub async fn handle_server_fns_with_context(
handle_server_fns_inner(fn_name, headers, query, additional_context, req)
.await
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn handle_server_fns_inner(
fn_name: String,
headers: HeaderMap,
@@ -323,15 +320,11 @@ async fn handle_server_fns_inner(
Encoding::Url | Encoding::Cbor => &req_parts.body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
match (server_fn.trait_obj)(cx, data).await {
let res = match (server_fn.trait_obj)(cx, data).await {
Ok(serialized) => {
// If ResponseOptions are set, add the headers and status to the request
let res_options = use_context::<ResponseOptions>(cx);
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
let accept_header = headers
.get("Accept")
@@ -396,7 +389,11 @@ async fn handle_server_fns_inner(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
)),
}
};
// clean up the scope
disposer.dispose();
runtime.dispose();
res
} else {
Response::builder().status(StatusCode::BAD_REQUEST).body(
Full::from(format!(
@@ -469,6 +466,7 @@ pub type PinnedHtmlStream =
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "info", fields(error), skip_all)]
pub fn render_app_to_stream<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@@ -542,6 +540,7 @@ where
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "info", fields(error), skip_all)]
pub fn render_app_to_stream_in_order<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@@ -587,6 +586,7 @@ where
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "info", fields(error), skip_all)]
pub fn render_app_to_stream_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
@@ -615,6 +615,8 @@ where
let res_options3 = default_res_options.clone();
let local_pool = get_leptos_pool();
let (tx, rx) = futures::channel::mpsc::channel(8);
let current_span = tracing::Span::current();
local_pool.spawn_pinned(move || async move {
let app = {
// Need to get the path and query string of the Request
@@ -638,12 +640,12 @@ where
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
});
}.instrument(current_span));
async move { generate_response(res_options3, rx).await }
})
}
}
#[tracing::instrument(level = "info", fields(error), skip_all)]
async fn generate_response(
res_options: ResponseOptions,
rx: Receiver<String>,
@@ -673,7 +675,7 @@ async fn generate_response(
res
}
#[tracing::instrument(level = "info", fields(error), skip_all)]
async fn forward_stream(
options: &LeptosOptions,
res_options2: ResponseOptions,
@@ -733,6 +735,7 @@ async fn forward_stream(
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "info", fields(error), skip_all)]
pub fn render_app_to_stream_in_order_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
@@ -770,7 +773,8 @@ where
let (tx, rx) = futures::channel::mpsc::channel(8);
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || async move {
let current_span = tracing::Span::current();
local_pool.spawn_pinned(|| async move {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
@@ -789,14 +793,14 @@ where
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
});
}.instrument(current_span));
generate_response(res_options3, rx).await
}
})
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn provide_contexts<B: 'static + std::fmt::Debug + std::default::Default>(
cx: Scope,
path: String,
@@ -864,6 +868,7 @@ fn provide_contexts<B: 'static + std::fmt::Debug + std::default::Default>(
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "info", fields(error), skip_all)]
pub fn render_app_async<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@@ -905,6 +910,7 @@ where
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "info", fields(error), skip_all)]
pub fn render_app_async_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
@@ -993,14 +999,15 @@ where
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<(String, SsrMode)>
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<(String, SsrMode)>>>);
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
let routes = Routes::default();
let routes_inner = routes.clone();
@@ -1024,17 +1031,26 @@ where
// Axum's Router defines Root routes as "/" not ""
let routes = routes
.into_iter()
.map(|(s, m)| {
if s.is_empty() {
("/".to_string(), m)
.map(|listing| {
let path = listing.path();
if path.is_empty() {
RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)
} else {
(s, m)
listing
}
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![("/".to_string(), Default::default())]
vec![RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)]
} else {
routes
}
@@ -1046,7 +1062,7 @@ pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -1055,7 +1071,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -1064,7 +1080,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
handler: H,
) -> Self
where
@@ -1074,10 +1090,11 @@ pub trait LeptosRoutes {
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl LeptosRoutes for axum::Router {
#[tracing::instrument(level = "info", fields(error), skip_all)]
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
@@ -1086,10 +1103,11 @@ impl LeptosRoutes for axum::Router {
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@@ -1097,38 +1115,66 @@ impl LeptosRoutes for axum::Router {
IV: IntoView + 'static,
{
let mut router = self;
for (path, mode) in paths.iter() {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
get(render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
))
}
SsrMode::InOrder => {
get(render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
))
}
SsrMode::Async => get(render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)),
},
);
for listing in paths.iter() {
let path = listing.path();
for method in listing.methods() {
router = router.route(
path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
},
);
}
}
router
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
handler: H,
) -> Self
where
@@ -1136,15 +1182,28 @@ impl LeptosRoutes for axum::Router {
T: 'static,
{
let mut router = self;
for (path, _) in paths.iter() {
router = router.route(path, get(handler.clone()));
for listing in paths.iter() {
for method in listing.methods() {
router = router.route(
listing.path(),
match method {
leptos_router::Method::Get => get(handler.clone()),
leptos_router::Method::Post => post(handler.clone()),
leptos_router::Method::Put => put(handler.clone()),
leptos_router::Method::Delete => {
delete(handler.clone())
}
leptos_router::Method::Patch => patch(handler.clone()),
},
);
}
}
router
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn get_leptos_pool() -> LocalPoolHandle {
static LOCAL_POOL: OnceLock<LocalPoolHandle> = OnceLock::new();
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();
LOCAL_POOL
.get_or_init(|| {
tokio_util::task::LocalPoolHandle::new(

View File

@@ -13,3 +13,4 @@ leptos = { workspace = true, features = ["ssr"] }
leptos_hot_reload = { workspace = true }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
tracing="0.1.37"

View File

@@ -3,6 +3,9 @@ use leptos::{use_context, RuntimeId, ScopeId};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
extern crate tracing;
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn autoreload(options: &LeptosOptions) -> String {
let site_ip = &options.site_addr.ip().to_string();
let reload_port = options.reload_port;
@@ -39,7 +42,7 @@ fn autoreload(options: &LeptosOptions) -> String {
false => "".to_string(),
}
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn html_parts(
options: &LeptosOptions,
meta: Option<&MetaContext>,
@@ -75,6 +78,7 @@ pub fn html_parts(
(head, tail)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn html_parts_separated(
options: &LeptosOptions,
meta: Option<&MetaContext>,
@@ -115,6 +119,7 @@ pub fn html_parts_separated(
(head, tail)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn build_async_response(
stream: impl Stream<Item = String> + 'static,
options: &LeptosOptions,

View File

@@ -216,16 +216,14 @@ async fn handle_server_fns_inner(
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
match (server_fn.trait_obj)(cx, data).await {
let res = match (server_fn.trait_obj)(cx, data)
.await
{
Ok(serialized) => {
// If ResponseOptions are set, add the headers and status to the request
let res_options =
use_context::<ResponseOptions>(cx);
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
let accept_header = headers
.get("Accept")
@@ -305,7 +303,11 @@ async fn handle_server_fns_inner(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
)),
}
};
// clean up the scope
disposer.dispose();
runtime.dispose();
res
} else {
Response::builder()
.status(StatusCode::BAD_REQUEST)
@@ -944,12 +946,12 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<(String, SsrMode)>
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<(String, SsrMode)>>>);
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
let routes = Routes::default();
let routes_inner = routes.clone();
@@ -973,17 +975,26 @@ where
// Viz's Router defines Root routes as "/" not ""
let routes = routes
.into_iter()
.map(|(s, m)| {
if s.is_empty() {
("/".to_string(), m)
.map(|listing| {
let path = listing.path();
if path.is_empty() {
RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)
} else {
(s, m)
listing
}
})
.collect::<Vec<_>>();
if routes.is_empty() {
vec![("/".to_string(), Default::default())]
vec![RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)]
} else {
routes
}
@@ -995,7 +1006,7 @@ pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
) -> Self
where
@@ -1004,7 +1015,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + Clone + Send + Sync + 'static,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
) -> Self
@@ -1013,7 +1024,7 @@ pub trait LeptosRoutes {
fn leptos_routes_with_handler<H, O>(
self,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
handler: H,
) -> Self
where
@@ -1026,7 +1037,7 @@ impl LeptosRoutes for Router {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
) -> Self
where
@@ -1038,52 +1049,93 @@ impl LeptosRoutes for Router {
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + Clone + Send + Sync + 'static,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + Sync + 'static,
) -> Self
where
IV: IntoView + 'static,
{
paths.iter().fold(self, |router, (path, mode)| match mode {
SsrMode::OutOfOrder => router.get(
path,
render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
),
SsrMode::InOrder => router.get(
path,
render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
),
SsrMode::Async => router.get(
path,
render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
),
paths.iter().fold(self, |router, listing| {
let path = listing.path();
let mode = listing.mode();
listing.methods().fold(router, |router, method| match mode {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
})
})
}
fn leptos_routes_with_handler<H, O>(
self,
paths: Vec<(String, SsrMode)>,
paths: Vec<RouteListing>,
handler: H,
) -> Self
where
H: Handler<Request, Output = Result<O>> + Clone,
O: IntoResponse + Send + Sync + 'static,
{
paths
.iter()
.fold(self, |router, (path, _)| router.get(path, handler.clone()))
paths.iter().fold(self, |router, listing| {
listing
.methods()
.fold(router, |router, method| match method {
leptos_router::Method::Get => {
router.get(listing.path(), handler.clone())
}
leptos_router::Method::Post => {
router.post(listing.path(), handler.clone())
}
leptos_router::Method::Put => {
router.put(listing.path(), handler.clone())
}
leptos_router::Method::Delete => {
router.delete(listing.path(), handler.clone())
}
leptos_router::Method::Patch => {
router.patch(listing.path(), handler.clone())
}
})
})
}
}

View File

@@ -13,11 +13,11 @@ cfg-if = "1"
leptos_dom = { workspace = true }
leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_server = { workspace = true, default-features = false }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.14"
server_fn = { workspace = true }
server_fn = { workspace = true, default-features = false }
[dev-dependencies]
leptos = { path = ".", default-features = false }
@@ -36,6 +36,8 @@ hydrate = [
"leptos_reactive/hydrate",
"leptos_server/hydrate",
]
default-tls = ["leptos_server/default-tls", "server_fn/default-tls"]
rustls = ["leptos_server/rustls", "server_fn/rustls"]
ssr = [
"leptos_dom/ssr",
"leptos_macro/ssr",
@@ -93,4 +95,8 @@ skip_feature_sets = [
"serde-lite",
"rkyv",
],
[
"default-tls",
"rustls",
],
]

View File

@@ -3,6 +3,7 @@ use crate::TextProp;
/// A collection of additional HTML attributes to be applied to an element,
/// each of which may or may not be reactive.
#[derive(Default, Clone)]
#[repr(transparent)]
pub struct AdditionalAttributes(pub(crate) Vec<(String, TextProp)>);
impl<I, T, U> From<I> for AdditionalAttributes
@@ -22,6 +23,7 @@ where
}
/// Iterator over additional HTML attributes.
#[repr(transparent)]
pub struct AdditionalAttributesIter<'a>(
std::slice::Iter<'a, (String, TextProp)>,
);
@@ -29,6 +31,7 @@ pub struct AdditionalAttributesIter<'a>(
impl<'a> Iterator for AdditionalAttributesIter<'a> {
type Item = &'a (String, TextProp);
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
@@ -39,6 +42,6 @@ impl<'a> IntoIterator for &'a AdditionalAttributes {
type IntoIter = AdditionalAttributesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
todo!()
AdditionalAttributesIter(self.0.iter())
}
}

View File

@@ -41,6 +41,10 @@ use std::hash::Hash;
/// }
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all)
)]
#[component(transparent)]
pub fn For<IF, I, T, EF, N, KF, K>(
cx: Scope,

View File

@@ -1,6 +1,5 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
//! # About Leptos
//!
//! Leptos is a full-stack framework for building web applications in Rust. You can use it to build
@@ -43,6 +42,7 @@
//! HTTP request within your reactive code.
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptoss nested router
//! to enable client-side navigation and route-specific, reactive data loading.
//! - [`slots`](https://github.com/leptos-rs/leptos/tree/main/examples/slots) shows how to use slots on components.
//! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows
//! different methods of interaction with a stateful server, including server functions, server actions, forms,
//! and server-sent events (SSE).
@@ -141,6 +141,8 @@
//! # }
//! ```
mod additional_attributes;
pub use additional_attributes::*;
pub use leptos_config::{self, get_configuration, LeptosOptions};
#[cfg(not(all(
target_arch = "wasm32",
@@ -158,7 +160,8 @@ pub use leptos_dom::{
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener,
window_event_listener, window_event_listener_untyped,
window_event_listener_with_precast,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
@@ -180,12 +183,13 @@ pub use for_loop::*;
pub use show::*;
mod suspense;
pub use suspense::*;
mod text_prop;
mod transition;
#[cfg(debug_assertions)]
pub use text_prop::TextProp;
#[cfg(any(debug_assertions, feature = "ssr"))]
#[doc(hidden)]
pub use tracing;
pub use transition::*;
extern crate self as leptos;
/// The most common type for the `children` property on components,
@@ -236,3 +240,24 @@ pub fn component_props_builder<P: Props>(
) -> <P as Props>::Builder {
<P as Props>::builder()
}
#[cfg(all(not(doc), feature = "csr", feature = "ssr"))]
compile_error!(
"You have both `csr` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently. `csr` is enabled by \
default on `leptos`, and can be disabled by adding `default-features = \
false` to your `leptos` dependency."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "ssr"))]
compile_error!(
"You have both `hydrate` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "csr"))]
compile_error!(
"You have both `hydrate` and `csr` enabled as features, which may cause \
issues. `csr` is enabled by default on `leptos`, and can be disabled by \
adding `default-features = false` to your `leptos` dependency."
);

View File

@@ -29,6 +29,10 @@ use std::{cell::RefCell, rc::Rc};
/// }
/// # });
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all)
)]
#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in

View File

@@ -50,6 +50,10 @@ use std::rc::Rc;
/// # });
/// # }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all)
)]
#[component(transparent)]
pub fn Suspense<F, E>(
cx: Scope,

43
leptos/src/text_prop.rs Normal file
View File

@@ -0,0 +1,43 @@
use std::{fmt::Debug, rc::Rc};
/// Describes a value that is either a static or a reactive string, i.e.,
/// a [String], a [&str], or a reactive `Fn() -> String`.
#[derive(Clone)]
pub struct TextProp(Rc<dyn Fn() -> String>);
impl TextProp {
/// Accesses the current value of the property.
#[inline(always)]
pub fn get(&self) -> String {
(self.0)()
}
}
impl Debug for TextProp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TextProp").finish()
}
}
impl From<String> for TextProp {
fn from(s: String) -> Self {
TextProp(Rc::new(move || s.clone()))
}
}
impl From<&str> for TextProp {
fn from(s: &str) -> Self {
let s = s.to_string();
TextProp(Rc::new(move || s.clone()))
}
}
impl<F> From<F> for TextProp
where
F: Fn() -> String + 'static,
{
#[inline(always)]
fn from(s: F) -> Self {
TextProp(Rc::new(s))
}
}

View File

@@ -60,6 +60,10 @@ use std::{
/// # });
/// # }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "info", skip_all)
)]
#[component(transparent)]
pub fn Transition<F, E>(
cx: Scope,

View File

@@ -54,7 +54,7 @@ pub struct ComponentRepr {
pub(crate) document_fragment: web_sys::DocumentFragment,
#[cfg(all(target_arch = "wasm32", feature = "web"))]
mounted: Rc<OnceCell<()>>,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) name: Cow<'static, str>,
#[cfg(debug_assertions)]
_opening: Comment,
@@ -135,13 +135,14 @@ impl Mountable for ComponentRepr {
};
}
#[inline]
fn get_closing_node(&self) -> web_sys::Node {
self.closing.node.clone()
}
}
impl IntoView for ComponentRepr {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "info", name = "<Component />", skip_all, fields(name = %self.name)))]
fn into_view(self, _: Scope) -> View {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
if !HydrationCtx::is_hydrating() {
@@ -156,17 +157,21 @@ impl IntoView for ComponentRepr {
impl ComponentRepr {
/// Creates a new [`Component`].
#[inline(always)]
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self::new_with_id(name, HydrationCtx::id())
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<Cow<'static, str>>,
id: HydrationKey,
) -> Self {
let name = name.into();
Self::new_with_id_concrete(name.into(), id)
}
fn new_with_id_concrete(name: Cow<'static, str>, id: HydrationKey) -> Self {
let markers = (
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
#[cfg(debug_assertions)]
@@ -202,7 +207,7 @@ impl ComponentRepr {
#[cfg(debug_assertions)]
_opening: markers.1,
closing: markers.0,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, feature = "ssr"))]
name,
children: Vec::with_capacity(1),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]

View File

@@ -139,13 +139,15 @@ where
/// 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]
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
#[inline(always)]
pub const fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
Self { id, child_fn }
}
}
@@ -156,11 +158,13 @@ where
N: IntoView,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "<DynChild />", skip_all)
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "<DynChild />", skip_all)
)]
#[inline]
fn into_view(self, cx: Scope) -> View {
// concrete inner function
#[inline(never)]
fn create_dyn_view(
cx: Scope,
component: DynChildRepr,
@@ -379,6 +383,7 @@ cfg_if! {
}
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)] {
@@ -395,6 +400,7 @@ cfg_if! {
}
}
#[cfg_attr(not(debug_assertions), inline(always))]
fn previous_non_view_marker_sibling(&self) -> Option<Node> {
cfg_if! {
if #[cfg(debug_assertions)] {

View File

@@ -155,6 +155,7 @@ impl Mountable for EachRepr {
};
}
#[inline(always)]
fn get_closing_node(&self) -> web_sys::Node {
self.closing.node.clone()
}
@@ -257,6 +258,7 @@ impl Mountable for EachItem {
}
}
#[inline(always)]
fn get_opening_node(&self) -> web_sys::Node {
#[cfg(debug_assertions)]
return self.opening.node.clone();
@@ -328,7 +330,8 @@ where
T: 'static,
{
/// Creates a new [`Each`] component.
pub fn new(items_fn: IF, key_fn: KF, each_fn: EF) -> Self {
#[inline(always)]
pub const fn new(items_fn: IF, key_fn: KF, each_fn: EF) -> Self {
Self {
items_fn,
each_fn,
@@ -348,8 +351,8 @@ where
T: 'static,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "<Each />", skip_all)
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "<Each />", skip_all)
)]
fn into_view(self, cx: Scope) -> crate::View {
let Self {

View File

@@ -1,20 +1,23 @@
use crate::{HydrationCtx, IntoView};
use cfg_if::cfg_if;
use leptos_reactive::{signal_prelude::*, use_context, RwSignal};
use std::{collections::HashMap, error::Error, sync::Arc};
use std::{borrow::Cow, collections::HashMap, error::Error, sync::Arc};
/// 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, Arc<dyn Error + Send + Sync>>);
/// A unique key for an error that occurs at a particular location in the user interface.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct ErrorKey(String);
#[repr(transparent)]
pub struct ErrorKey(Cow<'static, str>);
impl<T> From<T> for ErrorKey
where
T: Into<String>,
T: Into<Cow<'static, str>>,
{
#[inline(always)]
fn from(key: T) -> ErrorKey {
ErrorKey(key.into())
}
@@ -24,12 +27,14 @@ impl IntoIterator for Errors {
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
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,
@@ -40,6 +45,7 @@ pub struct IntoIter(
impl Iterator for IntoIter {
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
#[inline(always)]
fn next(
&mut self,
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
@@ -48,6 +54,7 @@ impl Iterator for IntoIter {
}
/// An iterator over all the errors contained in the [Errors] struct.
#[repr(transparent)]
pub struct Iter<'a>(
std::collections::hash_map::Iter<
'a,
@@ -59,6 +66,7 @@ pub struct Iter<'a>(
impl<'a> Iterator for Iter<'a> {
type Item = (&'a ErrorKey, &'a Arc<dyn Error + Send + Sync>);
#[inline(always)]
fn next(
&mut self,
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
@@ -72,7 +80,7 @@ where
E: Error + Send + Sync + 'static,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
let id = ErrorKey(HydrationCtx::peek().previous);
let id = ErrorKey(HydrationCtx::peek().previous.into());
let errors = use_context::<RwSignal<Errors>>(cx);
match self {
Ok(stuff) => {
@@ -127,6 +135,7 @@ where
}
impl Errors {
/// Returns `true` if there are no errors.
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
@@ -156,6 +165,7 @@ impl Errors {
}
/// An iterator over all the errors, in arbitrary order.
#[inline(always)]
pub fn iter(&self) -> Iter<'_> {
Iter(self.0.iter())
}

View File

@@ -14,6 +14,10 @@ where
I: IntoIterator<Item = V>,
V: IntoView,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", skip_all,)
)]
fn into_fragment(self, cx: Scope) -> Fragment {
self.into_iter().map(|v| v.into_view(cx)).collect()
}
@@ -43,17 +47,20 @@ impl From<View> for Fragment {
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>`].
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
#[inline(always)]
pub const fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
Self {
id,
nodes,
@@ -63,11 +70,13 @@ impl Fragment {
}
/// 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) -> &HydrationKey {
&self.id
}
@@ -81,7 +90,7 @@ impl Fragment {
}
impl IntoView for Fragment {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
#[cfg_attr(debug_assertions, instrument(level = "info", name = "</>", skip_all, fields(children = self.nodes.len())))]
fn into_view(self, cx: leptos_reactive::Scope) -> View {
let mut frag = ComponentRepr::new_with_id("", self.id.clone());

View File

@@ -40,14 +40,17 @@ impl Default for UnitRepr {
#[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()
}
@@ -59,8 +62,8 @@ pub struct Unit;
impl IntoView for Unit {
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "<() />", skip_all)
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "<() />", skip_all)
)]
fn into_view(self, _: leptos_reactive::Scope) -> crate::View {
let component = UnitRepr::default();

View File

@@ -14,6 +14,7 @@ thread_local! {
// 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,
@@ -21,8 +22,9 @@ pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
mut event_handler: impl FnMut(E::EventType) + 'static,
) {
let event_name = event.name();
let event_handler = Box::new(event_handler);
if event.bubbles() {
if E::BUBBLES {
add_event_listener(
target,
event.event_delegation_key(),
@@ -47,8 +49,8 @@ pub fn add_event_listener<E>(
target: &web_sys::Element,
key: Cow<'static, str>,
event_name: Cow<'static, str>,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
#[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,
@@ -56,16 +58,16 @@ pub fn add_event_listener<E>(
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |e| {
let cb = Box::new(move |e| {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
leptos_reactive::SpecialNonReactiveZone::exit();
};
});
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
let cb = Closure::wrap(cb as Box<dyn FnMut(E)>).into_js_value();
let key = intern(&key);
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
add_delegated_event_listener(&key, event_name, options);
@@ -76,26 +78,26 @@ pub fn add_event_listener<E>(
pub(crate) fn add_event_listener_undelegated<E>(
target: &web_sys::Element,
event_name: &str,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
#[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)] {
leptos_reactive::SpecialNonReactiveZone::enter();
let span = ::tracing::Span::current();
let cb = move |e| {
let cb = Box::new(move |e| {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb(e);
};
leptos_reactive::SpecialNonReactiveZone::exit();
leptos_reactive::SpecialNonReactiveZone::exit();
});
}
}
let event_name = intern(event_name);
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
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(

View File

@@ -8,23 +8,22 @@ 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) -> Cow<'static, str>;
/// The key used for event delegation.
fn event_delegation_key(&self) -> Cow<'static, str>;
/// Indicates if this event bubbles. For example, `click` bubbles,
/// but `focus` does not.
///
/// If this method returns true, then the event will be delegated globally,
/// otherwise, event listeners will be directly attached to the element.
fn bubbles(&self) -> bool {
true
}
/// 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
}
@@ -39,17 +38,17 @@ pub struct undelegated<Ev: EventDescriptor>(pub Ev);
impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
type EventType = Ev::EventType;
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
self.0.name()
}
#[inline(always)]
fn event_delegation_key(&self) -> Cow<'static, str> {
self.0.event_delegation_key()
}
fn bubbles(&self) -> bool {
false
}
const BUBBLES: bool = false;
}
/// A custom event.
@@ -80,10 +79,9 @@ impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
format!("$$${}", self.name).into()
}
fn bubbles(&self) -> bool {
false
}
const BUBBLES: bool = false;
#[inline(always)]
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
&self.options
}
@@ -142,24 +140,22 @@ macro_rules! generate_event_types {
impl EventDescriptor for $event {
type EventType = web_sys::$web_sys_event;
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
stringify!($event).into()
}
#[inline(always)]
fn event_delegation_key(&self) -> Cow<'static, str> {
concat!("$$$", stringify!($event)).into()
}
$(
generate_event_types!($does_not_bubble);
)?
const BUBBLES: bool = true $(&& generate_event_types!($does_not_bubble))?;
}
)*
};
(does_not_bubble) => {
fn bubbles(&self) -> bool { false }
}
(does_not_bubble) => { false }
}
generate_event_types! {

View File

@@ -1,6 +1,6 @@
//! A variety of DOM utility functions.
use crate::{is_server, window};
use crate::{events::typed as ev, is_server, window};
use leptos_reactive::{on_cleanup, Scope};
use std::time::Duration;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
@@ -97,6 +97,7 @@ impl AnimationFrameRequestHandle {
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
_ = request_animation_frame_with_handle(cb);
}
@@ -105,6 +106,7 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
/// returning a cancelable handle.
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame_with_handle(
cb: impl FnOnce() + 'static,
) -> Result<AnimationFrameRequestHandle, JsValue> {
@@ -118,10 +120,14 @@ pub fn request_animation_frame_with_handle(
}
}
let cb = Closure::once_into_js(cb);
window()
.request_animation_frame(cb.as_ref().unchecked_ref())
.map(AnimationFrameRequestHandle)
#[inline(never)]
fn raf(cb: JsValue) -> Result<AnimationFrameRequestHandle, JsValue> {
window()
.request_animation_frame(cb.as_ref().unchecked_ref())
.map(AnimationFrameRequestHandle)
}
raf(Closure::once_into_js(cb))
}
/// Handle that is generated by [request_idle_callback_with_handle] and can be
@@ -140,6 +146,7 @@ impl IdleCallbackHandle {
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
_ = request_idle_callback_with_handle(cb);
}
@@ -148,6 +155,7 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
/// returning a cancelable handle.
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback_with_handle(
cb: impl Fn() + 'static,
) -> Result<IdleCallbackHandle, JsValue> {
@@ -161,10 +169,16 @@ pub fn request_idle_callback_with_handle(
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
window()
.request_idle_callback(cb.as_ref().unchecked_ref())
.map(IdleCallbackHandle)
#[inline(never)]
fn ric(cb: Box<dyn Fn()>) -> Result<IdleCallbackHandle, JsValue> {
let cb = Closure::wrap(cb).into_js_value();
window()
.request_idle_callback(cb.as_ref().unchecked_ref())
.map(IdleCallbackHandle)
}
ric(Box::new(cb))
}
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
@@ -182,7 +196,7 @@ impl TimeoutHandle {
/// Executes the given function after the given duration of time has passed.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
#[cfg_attr(
debug_assertions,
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
@@ -192,9 +206,10 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
#[cfg_attr(
debug_assertions,
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_timeout_with_handle(
cb: impl FnOnce() + 'static,
duration: Duration,
@@ -211,13 +226,17 @@ pub fn set_timeout_with_handle(
}
}
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
window()
.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)
.map(TimeoutHandle)
#[inline(never)]
fn st(cb: JsValue, duration: Duration) -> Result<TimeoutHandle, JsValue> {
window()
.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)
.map(TimeoutHandle)
}
st(Closure::once_into_js(cb), duration)
}
/// "Debounce" a callback function. This will cause it to wait for a period of `delay`
@@ -243,7 +262,8 @@ pub fn set_timeout_with_handle(
pub fn debounce<T: 'static>(
cx: Scope,
delay: Duration,
#[allow(unused_mut)] mut cb: impl FnMut(T) + 'static,
#[cfg(debug_assertions)] mut cb: impl FnMut(T) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(T) + 'static,
) -> impl FnMut(T) {
use std::{
cell::{Cell, RefCell},
@@ -309,7 +329,7 @@ impl IntervalHandle {
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[deprecated = "use set_interval_with_handle() instead. In the future, \
@@ -344,9 +364,10 @@ pub fn set_interval(
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_interval_with_handle(
cb: impl Fn() + 'static,
duration: Duration,
@@ -363,21 +384,49 @@ pub fn set_interval_with_handle(
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
let handle = window()
.set_interval_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)?;
Ok(IntervalHandle(handle))
#[inline(never)]
fn si(
cb: Box<dyn Fn()>,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
let cb = Closure::wrap(cb).into_js_value();
window()
.set_interval_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)
.map(IntervalHandle)
}
si(Box::new(cb), duration)
}
/// Adds an event listener to the `Window`.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all, fields(event_name = %event_name))
)]
#[inline(always)]
#[deprecated = "In the next release, `window_event_listener` will become \
typed. You can switch now to `window_event_listener_untyped` \
for the current behavior or use \
`window_event_listener_with_precast`, which will become the \
new`window_event_listener`."]
pub fn window_event_listener(
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
) {
window_event_listener_untyped(event_name, cb)
}
/// Adds an event listener to the `Window`, typed as a generic `Event`.
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(event_name = %event_name))
)]
pub fn window_event_listener(
#[inline(always)]
pub fn window_event_listener_untyped(
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
) {
@@ -394,14 +443,31 @@ pub fn window_event_listener(
}
if !is_server() {
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
#[inline(never)]
fn wel(cb: Box<dyn FnMut(web_sys::Event)>, event_name: &str) {
let cb = Closure::wrap(cb).into_js_value();
_ = window().add_event_listener_with_callback(
event_name,
cb.unchecked_ref(),
);
}
let cb = Closure::wrap(handler).into_js_value();
_ = window()
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
wel(Box::new(cb), event_name);
}
}
/// Creates a window event listener where the event in the callback is already appropriately cast.
pub fn window_event_listener_with_precast<E: ev::EventDescriptor + 'static>(
event: E,
cb: impl Fn(E::EventType) + 'static,
) where
E::EventType: JsCast,
{
window_event_listener_untyped(&event.name(), move |e| {
cb(e.unchecked_into::<E::EventType>())
});
}
#[doc(hidden)]
/// This exists only to enable type inference on event listeners when in SSR mode.
pub fn ssr_event_listener<E: crate::ev::EventDescriptor + 'static>(

View File

@@ -75,6 +75,7 @@ pub trait ElementDescriptor: ElementDescriptorBounds {
fn name(&self) -> Cow<'static, str>;
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
#[inline(always)]
fn is_void(&self) -> bool {
false
}
@@ -140,6 +141,7 @@ pub struct AnyElement {
impl std::ops::Deref for AnyElement {
type Target = web_sys::HtmlElement;
#[inline(always)]
fn deref(&self) -> &Self::Target {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return &self.element;
@@ -150,6 +152,7 @@ impl std::ops::Deref for AnyElement {
}
impl std::convert::AsRef<web_sys::HtmlElement> for AnyElement {
#[inline(always)]
fn as_ref(&self) -> &web_sys::HtmlElement {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return &self.element;
@@ -164,11 +167,13 @@ impl ElementDescriptor for AnyElement {
self.name.clone()
}
#[inline(always)]
fn is_void(&self) -> bool {
self.is_void
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
#[inline(always)]
fn hydration_id(&self) -> &HydrationKey {
&self.id
}
@@ -254,6 +259,7 @@ impl Custom {
impl std::ops::Deref for Custom {
type Target = web_sys::HtmlElement;
#[inline(always)]
fn deref(&self) -> &Self::Target {
&self.element
}
@@ -261,6 +267,7 @@ impl std::ops::Deref for Custom {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl std::convert::AsRef<web_sys::HtmlElement> for Custom {
#[inline(always)]
fn as_ref(&self) -> &web_sys::HtmlElement {
&self.element
}
@@ -272,6 +279,7 @@ impl ElementDescriptor for Custom {
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
#[inline(always)]
fn hydration_id(&self) -> &HydrationKey {
&self.id
}
@@ -413,6 +421,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(debug_assertions)]
/// Adds an optional marker indicating the view macro source.
#[inline(always)]
pub fn with_view_marker(mut self, marker: impl Into<String>) -> Self {
self.view_marker = Some(marker.into());
self
@@ -471,15 +480,18 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// Adds an `id` to the element.
#[track_caller]
#[inline(always)]
pub fn id(self, id: impl Into<Cow<'static, str>>) -> Self {
let id = id.into();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
self.element
.as_ref()
.set_attribute(wasm_bindgen::intern("id"), &id)
.unwrap();
#[inline(never)]
fn id_inner(el: &web_sys::HtmlElement, id: &str) {
el.set_attribute(wasm_bindgen::intern("id"), id).unwrap()
}
id_inner(self.element.as_ref(), &id);
self
}
@@ -495,6 +507,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
/// Binds the element reference to [`NodeRef`].
#[inline(always)]
pub fn node_ref(self, node_ref: NodeRef<El>) -> Self
where
Self: Clone,
@@ -577,13 +590,16 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// of `body`.
///
/// This method will always return [`None`] on non-wasm CSR targets.
#[inline(always)]
pub fn is_mounted(&self) -> bool {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
crate::document()
.body()
.unwrap()
.contains(Some(self.element.as_ref()))
#[inline(never)]
fn is_mounted_inner(el: &web_sys::HtmlElement) -> bool {
crate::document().body().unwrap().contains(Some(el))
}
is_mounted_inner(self.element.as_ref())
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
@@ -592,6 +608,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// Adds an attribute to this element.
#[track_caller]
#[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))]
pub fn attr(
self,
name: impl Into<Cow<'static, str>>,
@@ -621,7 +638,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
match attr {
Attribute::String(value) => {
this.attrs.push((name, value.into()));
this.attrs.push((name, value));
}
Attribute::Bool(include) => {
if include {
@@ -630,7 +647,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
Attribute::Option(_, maybe) => {
if let Some(value) = maybe {
this.attrs.push((name, value.into()));
this.attrs.push((name, value));
}
}
_ => unreachable!(),
@@ -641,6 +658,15 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
/// Adds a class to an element.
///
/// **Note**: In the builder syntax, this will be overwritten by the `class`
/// attribute if you use `.attr("class", /* */)`. In the `view` macro, they
/// are automatically re-ordered so that this over-writing does not happen.
///
/// # Panics
/// This directly uses the browsers `classList` API, which means it will throw
/// a runtime error if you pass more than a single class name. If you want to
/// pass more than one class name at a time, you can use [HtmlElement::classes].
#[track_caller]
pub fn class(
self,
@@ -685,10 +711,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
}
/// Adds a list of classes separated by ASCII whitespace to an element.
#[track_caller]
pub fn classes(self, classes: impl Into<Cow<'static, str>>) -> Self {
let classes = classes.into();
fn classes_inner(self, classes: &str) -> Self {
let mut this = self;
for class in classes.split_ascii_whitespace() {
this = this.class(class.to_string(), true);
@@ -696,6 +719,13 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
this
}
/// Adds a list of classes separated by ASCII whitespace to an element.
#[track_caller]
#[inline(always)]
pub fn classes(self, classes: impl Into<Cow<'static, str>>) -> Self {
self.classes_inner(&classes.into())
}
/// Sets the class on the element as the class signal changes.
#[track_caller]
pub fn dyn_classes<I, C>(
@@ -820,6 +850,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// Adds an event listener to this element.
#[track_caller]
#[inline(always)]
pub fn on<E: EventDescriptor + 'static>(
self,
event: E,
@@ -842,8 +873,9 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
let event_name = event.name();
let key = event.event_delegation_key();
let event_handler = Box::new(event_handler);
if event.bubbles() {
if E::BUBBLES {
add_event_listener(
self.element.as_ref(),
key,
@@ -922,6 +954,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
/// Be very careful when using this method. Always remember to
/// sanitize the input to avoid a cross-site scripting (XSS)
/// vulnerability.
#[inline(always)]
pub fn inner_html(self, html: impl Into<Cow<'static, str>>) -> Self {
let html = html.into();
@@ -944,7 +977,8 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<HtmlElement />", skip_all, fields(tag = %self.element.name())))]
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "trace", name = "<HtmlElement />", skip_all, fields(tag = %self.element.name())))]
#[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))]
fn into_view(self, _: Scope) -> View {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
@@ -987,7 +1021,7 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
impl<El: ElementDescriptor, const N: usize> IntoView for [HtmlElement<El>; N] {
#[cfg_attr(
debug_assertions,
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "[HtmlElement; N]", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
@@ -1011,6 +1045,7 @@ pub fn custom<El: ElementDescriptor>(cx: Scope, el: El) -> HtmlElement<Custom> {
}
/// Creates a text node.
#[inline(always)]
pub fn text(text: impl Into<Cow<'static, str>>) -> Text {
Text::new(text.into())
}
@@ -1072,6 +1107,7 @@ macro_rules! generate_html_tags {
impl std::ops::Deref for [<$tag:camel $($trailing_)?>] {
type Target = web_sys::$el_type;
#[inline(always)]
fn deref(&self) -> &Self::Target {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
@@ -1085,6 +1121,7 @@ macro_rules! generate_html_tags {
}
impl std::convert::AsRef<web_sys::HtmlElement> for [<$tag:camel $($trailing_)?>] {
#[inline(always)]
fn as_ref(&self) -> &web_sys::HtmlElement {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return &self.element;
@@ -1095,11 +1132,13 @@ macro_rules! generate_html_tags {
}
impl ElementDescriptor for [<$tag:camel $($trailing_)?>] {
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
stringify!($tag).into()
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
#[inline(always)]
fn hydration_id(&self) -> &HydrationKey {
&self.id
}
@@ -1109,7 +1148,7 @@ macro_rules! generate_html_tags {
#[$meta]
#[cfg_attr(
debug_assertions,
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "HtmlElement",
@@ -1127,6 +1166,7 @@ macro_rules! generate_html_tags {
};
(@void) => {};
(@void void) => {
#[inline(always)]
fn is_void(&self) -> bool {
true
}

View File

@@ -6,7 +6,7 @@
//! The DOM implementation for `leptos`.
#[doc(hidden)]
#[cfg_attr(debug_assertions, macro_use)]
#[cfg_attr(any(debug_assertions, feature = "ssr"), macro_use)]
pub extern crate tracing;
mod components;
@@ -93,8 +93,8 @@ pub trait Mountable {
impl IntoView for () {
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "<() />", skip_all)
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "<() />", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
Unit.into_view(cx)
@@ -106,8 +106,8 @@ where
T: IntoView,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "Option<T>", skip_all)
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "Option<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
if let Some(t) = self {
@@ -124,8 +124,8 @@ where
N: IntoView,
{
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "Fn() -> impl IntoView", skip_all)
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "Fn() -> impl IntoView", skip_all)
)]
#[track_caller]
fn into_view(self, cx: Scope) -> View {
@@ -137,6 +137,7 @@ impl<T> IntoView for (Scope, T)
where
T: IntoView,
{
#[inline(always)]
fn into_view(self, _: Scope) -> View {
self.1.into_view(self.0)
}
@@ -148,7 +149,7 @@ where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "ReadSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
@@ -161,7 +162,7 @@ where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "RwSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
@@ -174,7 +175,7 @@ where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "Memo<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
@@ -187,7 +188,7 @@ where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "Signal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
@@ -200,7 +201,7 @@ where
T: IntoView + Clone,
{
#[cfg_attr(
debug_assertions,
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", name = "MaybeSignal<T>", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
@@ -331,7 +332,7 @@ impl Element {
}
impl IntoView for Element {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Element />", skip_all, fields(tag = %self.name)))]
#[cfg_attr(debug_assertions, instrument(level = "info", name = "<Element />", skip_all, fields(tag = %self.name)))]
fn into_view(self, _: Scope) -> View {
View::Element(self)
}
@@ -373,48 +374,53 @@ struct Comment {
}
impl Comment {
#[inline]
fn new(
content: impl Into<Cow<'static, str>>,
id: &HydrationKey,
closing: bool,
) -> Self {
let content = content.into();
Self::new_inner(content.into(), id, closing)
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let _ = id;
let _ = closing;
}
fn new_inner(
content: Cow<'static, str>,
id: &HydrationKey,
closing: bool,
) -> Self {
cfg_if! {
if #[cfg(not(all(target_arch = "wasm32", feature = "web")))] {
let _ = id;
let _ = closing;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let node = COMMENT.with(|comment| comment.clone_node().unwrap());
Self { content }
} else {
let node = COMMENT.with(|comment| comment.clone_node().unwrap());
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
node.set_text_content(Some(&format!(" {content} ")));
#[cfg(debug_assertions)]
node.set_text_content(Some(&format!(" {content} ")));
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
if HydrationCtx::is_hydrating() {
let id = HydrationCtx::to_string(id, closing);
if HydrationCtx::is_hydrating() {
let id = HydrationCtx::to_string(id, closing);
if let Some(marker) = hydration::get_marker(&id) {
marker.before_with_node_1(&node).unwrap();
if let Some(marker) = hydration::get_marker(&id) {
marker.before_with_node_1(&node).unwrap();
marker.remove();
} else {
crate::warn!(
"component with id {id} not found, ignoring it for \
hydration"
);
marker.remove();
} else {
crate::warn!(
"component with id {id} not found, ignoring it for \
hydration"
);
}
}
Self {
node,
content,
}
}
}
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
node,
content,
}
}
}
@@ -437,7 +443,7 @@ impl fmt::Debug for Text {
}
impl IntoView for Text {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "#text", skip_all, fields(content = %self.content)))]
#[cfg_attr(debug_assertions, instrument(level = "info", name = "#text", skip_all, fields(content = %self.content)))]
fn into_view(self, _: Scope) -> View {
View::Text(self)
}
@@ -499,7 +505,7 @@ impl Default for View {
}
impl IntoView for View {
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "Node", skip_all, fields(kind = self.kind_name())))]
#[cfg_attr(debug_assertions, instrument(level = "info", name = "Node", skip_all, fields(kind = self.kind_name())))]
fn into_view(self, _: Scope) -> View {
self
}
@@ -513,8 +519,8 @@ impl IntoView for &View {
impl<const N: usize> IntoView for [View; N] {
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "[Node; N]", skip_all)
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "[Node; N]", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
Fragment::new(self.into_iter().collect()).into_view(cx)
@@ -652,6 +658,7 @@ impl View {
///
/// This method will attach an event listener to **all** child
/// [`HtmlElement`] children.
#[inline(always)]
pub fn on<E: ev::EventDescriptor + 'static>(
self,
event: E,
@@ -680,7 +687,7 @@ impl View {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
match &self {
Self::Element(el) => {
if event.bubbles() {
if E::BUBBLES {
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler, &None);
} else {
add_event_listener_undelegated(
@@ -919,6 +926,7 @@ macro_rules! impl_into_view_for_tuples {
where
$($ty: IntoView),*
{
#[inline]
fn into_view(self, cx: Scope) -> View {
paste::paste! {
let ($([<$ty:lower>],)*) = self;
@@ -990,15 +998,21 @@ api_planning! {
impl IntoView for String {
#[cfg_attr(
debug_assertions,
instrument(level = "trace", name = "#text", skip_all)
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "#text", skip_all)
)]
#[inline(always)]
fn into_view(self, _: Scope) -> View {
View::Text(Text::new(self.into()))
}
}
impl IntoView for &'static str {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "#text", skip_all)
)]
#[inline(always)]
fn into_view(self, _: Scope) -> View {
View::Text(Text::new(self.into()))
}
@@ -1008,6 +1022,10 @@ impl<V> IntoView for Vec<V>
where
V: IntoView,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "#text", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
self.into_iter()
.map(|v| v.into_view(cx))
@@ -1020,6 +1038,7 @@ macro_rules! viewable_primitive {
($($child_type:ty),* $(,)?) => {
$(
impl IntoView for $child_type {
#[inline(always)]
fn into_view(self, _cx: Scope) -> View {
View::Text(Text::new(self.to_string().into()))
}

View File

@@ -1,5 +1,5 @@
use leptos_reactive::Scope;
use std::rc::Rc;
use std::{borrow::Cow, rc::Rc};
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use wasm_bindgen::UnwrapThrowExt;
@@ -11,11 +11,11 @@ use wasm_bindgen::UnwrapThrowExt;
#[derive(Clone)]
pub enum Attribute {
/// A plain string value.
String(String),
String(Cow<'static, str>),
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
Fn(Scope, Rc<dyn Fn() -> Attribute>),
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
Option(Scope, Option<String>),
Option(Scope, Option<Cow<'static, str>>),
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
Bool(bool),
}
@@ -23,9 +23,14 @@ pub enum Attribute {
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) -> String {
pub fn as_value_string(
&self,
attr_name: &'static str,
) -> Cow<'static, str> {
match self {
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
Attribute::String(value) => {
format!("{attr_name}=\"{value}\"").into()
}
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
@@ -35,23 +40,19 @@ impl Attribute {
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| format!("{attr_name}=\"{value}\""))
.map(|value| format!("{attr_name}=\"{value}\"").into())
.unwrap_or_default(),
Attribute::Bool(include) => {
if *include {
attr_name.to_string()
} else {
String::new()
}
Cow::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<String> {
pub fn as_nameless_value_string(&self) -> Option<Cow<'static, str>> {
match self {
Attribute::String(value) => Some(value.to_string()),
Attribute::String(value) => Some(value.clone()),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
@@ -59,12 +60,10 @@ impl Attribute {
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => {
value.as_ref().map(|value| value.to_string())
}
Attribute::Option(_, value) => value.as_ref().cloned(),
Attribute::Bool(include) => {
if *include {
Some("".to_string())
Some("".into())
} else {
None
}
@@ -109,18 +108,19 @@ pub trait IntoAttribute {
}
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]
#[inline(always)]
fn into_attribute(self, _: Scope) -> Attribute {
self
}
#[inline]
#[inline(always)]
fn into_attribute_boxed(self: Box<Self>, _: Scope) -> Attribute {
*self
}
@@ -128,7 +128,7 @@ impl IntoAttribute for Attribute {
macro_rules! impl_into_attr_boxed {
() => {
#[inline]
#[inline(always)]
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
self.into_attribute(cx)
}
@@ -136,6 +136,7 @@ macro_rules! impl_into_attr_boxed {
}
impl IntoAttribute for Option<Attribute> {
#[inline(always)]
fn into_attribute(self, cx: Scope) -> Attribute {
self.unwrap_or(Attribute::Option(cx, None))
}
@@ -144,6 +145,25 @@ impl IntoAttribute for Option<Attribute> {
}
impl IntoAttribute for String {
#[inline(always)]
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(Cow::Owned(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for &'static str {
#[inline(always)]
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(Cow::Borrowed(self))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Cow<'static, str> {
#[inline(always)]
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(self)
}
@@ -152,6 +172,7 @@ impl IntoAttribute for String {
}
impl IntoAttribute for bool {
#[inline(always)]
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::Bool(self)
}
@@ -160,6 +181,25 @@ impl IntoAttribute for bool {
}
impl IntoAttribute for Option<String> {
#[inline(always)]
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(Cow::Owned))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<&'static str> {
#[inline(always)]
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(Cow::Borrowed))
}
impl_into_attr_boxed! {}
}
impl IntoAttribute for Option<Cow<'static, str>> {
#[inline(always)]
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self)
}
@@ -181,6 +221,7 @@ where
}
impl<T: IntoAttribute> IntoAttribute for (Scope, T) {
#[inline(always)]
fn into_attribute(self, _: Scope) -> Attribute {
self.1.into_attribute(self.0)
}
@@ -200,6 +241,7 @@ impl IntoAttribute for (Scope, Option<Box<dyn IntoAttribute>>) {
}
impl IntoAttribute for (Scope, Box<dyn IntoAttribute>) {
#[inline(always)]
fn into_attribute(self, _: Scope) -> Attribute {
self.1.into_attribute_boxed(self.0)
}
@@ -211,7 +253,7 @@ macro_rules! attr_type {
($attr_type:ty) => {
impl IntoAttribute for $attr_type {
fn into_attribute(self, _: Scope) -> Attribute {
Attribute::String(self.to_string())
Attribute::String(self.to_string().into())
}
#[inline]
@@ -222,7 +264,7 @@ macro_rules! attr_type {
impl IntoAttribute for Option<$attr_type> {
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(|n| n.to_string()))
Attribute::Option(cx, self.map(|n| n.to_string().into()))
}
#[inline]
@@ -234,7 +276,6 @@ macro_rules! attr_type {
}
attr_type!(&String);
attr_type!(&str);
attr_type!(usize);
attr_type!(u8);
attr_type!(u16);
@@ -251,10 +292,9 @@ attr_type!(f32);
attr_type!(f64);
attr_type!(char);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
pub fn attribute_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
@@ -277,6 +317,7 @@ pub fn attribute_helper(
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,

View File

@@ -21,6 +21,7 @@ pub trait IntoClass {
}
impl IntoClass for bool {
#[inline(always)]
fn into_class(self, _cx: Scope) -> Class {
Class::Value(self)
}
@@ -30,6 +31,7 @@ impl<T> IntoClass for T
where
T: Fn() -> bool + 'static,
{
#[inline(always)]
fn into_class(self, cx: Scope) -> Class {
let modified_fn = Box::new(self);
Class::Fn(cx, modified_fn)
@@ -60,6 +62,7 @@ impl Class {
}
impl<T: IntoClass> IntoClass for (Scope, T) {
#[inline(always)]
fn into_class(self, _: Scope) -> Class {
self.1.into_class(self.0)
}
@@ -70,6 +73,7 @@ use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
pub fn class_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
@@ -95,6 +99,7 @@ pub fn class_helper(
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn class_expression(
class_list: &web_sys::DomTokenList,
class_name: &str,

View File

@@ -36,6 +36,7 @@ where
}
impl<T: IntoProperty> IntoProperty for (Scope, T) {
#[inline(always)]
fn into_property(self, _: Scope) -> Property {
self.1.into_property(self.0)
}
@@ -44,12 +45,14 @@ impl<T: IntoProperty> IntoProperty for (Scope, T) {
macro_rules! prop_type {
($prop_type:ty) => {
impl IntoProperty for $prop_type {
#[inline(always)]
fn into_property(self, _cx: Scope) -> Property {
Property::Value(self.into())
}
}
impl IntoProperty for Option<$prop_type> {
#[inline(always)]
fn into_property(self, _cx: Scope) -> Property {
Property::Value(self.into())
}
@@ -81,6 +84,7 @@ prop_type!(bool);
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn property_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
@@ -106,6 +110,7 @@ pub(crate) fn property_helper(
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[inline(never)]
pub(crate) fn property_expression(
el: &web_sys::Element,
prop_name: &str,

View File

@@ -35,6 +35,7 @@ use std::cell::Cell;
/// }
/// }
/// ```
#[repr(transparent)]
pub struct NodeRef<T: ElementDescriptor + 'static>(
RwSignal<Option<HtmlElement<T>>>,
);
@@ -70,6 +71,7 @@ pub struct NodeRef<T: ElementDescriptor + 'static>(
/// }
/// }
/// ```
#[inline(always)]
pub fn create_node_ref<T: ElementDescriptor + 'static>(
cx: Scope,
) -> NodeRef<T> {
@@ -89,6 +91,7 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
/// 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,
@@ -96,6 +99,18 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
self.0.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.0.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,
@@ -120,6 +135,7 @@ impl<T: ElementDescriptor + 'static> NodeRef<T> {
/// Runs the provided closure when the `NodeRef` has been connected
/// with it's [`HtmlElement`].
#[inline(always)]
pub fn on_load<F>(self, cx: Scope, f: F)
where
T: Clone,
@@ -148,18 +164,21 @@ cfg_if::cfg_if! {
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

@@ -26,6 +26,10 @@ type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
/// assert!(html.contains("Hello, world!</p>"));
/// # }}
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", skip_all,)
)]
pub fn render_to_string<F, N>(f: F) -> String
where
F: FnOnce(Scope) -> N + 'static,
@@ -55,6 +59,10 @@ where
/// 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 = "info", skip_all,)
)]
pub fn render_to_stream(
view: impl FnOnce(Scope) -> View + 'static,
) -> impl Stream<Item = String> {
@@ -75,6 +83,10 @@ pub fn render_to_stream(
/// 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 = "info", skip_all,)
)]
pub fn render_to_stream_with_prefix(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
@@ -100,6 +112,10 @@ pub fn render_to_stream_with_prefix(
/// 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 = "info", skip_all,)
)]
pub fn render_to_stream_with_prefix_undisposed(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
@@ -122,6 +138,10 @@ pub fn render_to_stream_with_prefix_undisposed(
/// 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 = "info", skip_all,)
)]
pub fn render_to_stream_with_prefix_undisposed_with_context(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
@@ -209,7 +229,10 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
(stream, runtime, scope)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
fn fragments_to_chunks(
fragments: impl Stream<Item = (String, String)>,
) -> impl Stream<Item = String> {
@@ -243,20 +266,37 @@ fn fragments_to_chunks(
impl View {
/// Consumes the node and renders it into an HTML string.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", skip_all,)
)]
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
self.render_to_string_helper()
self.render_to_string_helper(false)
}
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn render_to_string_helper(
self,
dont_escape_text: bool,
) -> Cow<'static, str> {
match self {
View::Text(node) => {
html_escape::encode_safe(&node.content).to_string().into()
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())
.map(|node| {
node.render_to_string_helper(dont_escape_text)
})
.join("")
};
cfg_if! {
@@ -283,7 +323,8 @@ impl View {
}
View::Suspense(id, node) => format!(
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
View::CoreComponent(node).render_to_string_helper()
View::CoreComponent(node)
.render_to_string_helper(dont_escape_text)
)
.into(),
View::CoreComponent(node) => {
@@ -333,7 +374,9 @@ impl View {
t.content
}
} else {
child.render_to_string_helper()
child.render_to_string_helper(
dont_escape_text,
)
}
} else {
"".into()
@@ -356,7 +399,9 @@ impl View {
let id = node.id;
let content = || {
node.child.render_to_string_helper()
node.child.render_to_string_helper(
dont_escape_text,
)
};
#[cfg(debug_assertions)]
@@ -409,6 +454,8 @@ impl View {
}
}
View::Element(el) => {
let is_script_or_style =
el.name == "script" || el.name == "style";
let el_html = if let ElementChildren::Chunks(chunks) =
el.children
{
@@ -416,9 +463,8 @@ impl View {
.into_iter()
.map(|chunk| match chunk {
StringOrView::String(string) => string,
StringOrView::View(view) => {
view().render_to_string_helper()
}
StringOrView::View(view) => view()
.render_to_string_helper(is_script_or_style),
})
.join("")
.into()
@@ -460,7 +506,11 @@ impl View {
ElementChildren::Empty => "".into(),
ElementChildren::Children(c) => c
.into_iter()
.map(View::render_to_string_helper)
.map(|v| {
v.render_to_string_helper(
is_script_or_style,
)
})
.join("")
.into(),
ElementChildren::InnerHtml(h) => h,
@@ -524,7 +574,10 @@ pub(crate) fn to_kebab_case(name: &str) -> String {
new_name
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub(crate) fn render_serializers(
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> impl Stream<Item = String> {

View File

@@ -19,6 +19,7 @@ use std::{borrow::Cow, 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 = "info", skip_all)]
pub async fn render_to_string_async(
view: impl FnOnce(Scope) -> View + 'static,
) -> String {
@@ -34,6 +35,7 @@ pub async fn render_to_string_async(
/// 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 = "info", skip_all)]
pub fn render_to_stream_in_order(
view: impl FnOnce(Scope) -> View + 'static,
) -> impl Stream<Item = String> {
@@ -48,6 +50,7 @@ pub fn render_to_stream_in_order(
///
/// `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(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
@@ -70,6 +73,7 @@ pub fn render_to_stream_in_order_with_prefix(
///
/// `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(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
@@ -144,6 +148,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
(stream, runtime, scope_id)
}
#[tracing::instrument(level = "trace", skip_all)]
#[async_recursion(?Send)]
async fn handle_blocking_chunks(
tx: UnboundedSender<String>,
@@ -184,6 +189,7 @@ async fn handle_blocking_chunks(
queued_chunks
}
#[tracing::instrument(level = "trace", skip_all)]
#[async_recursion(?Send)]
async fn handle_chunks(
tx: UnboundedSender<String>,
@@ -211,16 +217,18 @@ async fn handle_chunks(
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, cx: Scope) -> VecDeque<StreamChunk> {
let mut chunks = VecDeque::new();
self.into_stream_chunks_helper(cx, &mut chunks);
self.into_stream_chunks_helper(cx, &mut chunks, false);
chunks
}
#[tracing::instrument(level = "trace", skip_all)]
fn into_stream_chunks_helper(
self,
cx: Scope,
chunks: &mut VecDeque<StreamChunk>,
dont_escape_text: bool,
) {
match self {
View::Suspense(id, _) => {
@@ -241,18 +249,21 @@ impl View {
let name = crate::ssr::to_kebab_case(&node.name);
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-start-->"#, HydrationCtx::to_string(&node.id, false)).into()));
for child in node.children {
child.into_stream_chunks_helper(cx, chunks);
child.into_stream_chunks_helper(cx, chunks, dont_escape_text);
}
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
} else {
for child in node.children {
child.into_stream_chunks_helper(cx, chunks);
child.into_stream_chunks_helper(cx, chunks, dont_escape_text);
}
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
}
}
}
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(
@@ -266,7 +277,11 @@ impl View {
chunks.push_back(StreamChunk::Sync(string))
}
StringOrView::View(view) => {
view().into_stream_chunks_helper(cx, chunks);
view().into_stream_chunks_helper(
cx,
chunks,
is_script_or_style,
);
}
}
}
@@ -318,7 +333,11 @@ impl View {
ElementChildren::Empty => {}
ElementChildren::Children(children) => {
for child in children {
child.into_stream_chunks_helper(cx, chunks);
child.into_stream_chunks_helper(
cx,
chunks,
is_script_or_style,
);
}
}
ElementChildren::InnerHtml(inner_html) => {
@@ -387,22 +406,33 @@ impl View {
// into one single node, so we need to artificially make the
// browser create the dynamic text as it's own text node
if let View::Text(t) = child {
let content = if dont_escape_text {
t.content
} else {
html_escape::encode_safe(
&t.content,
)
.to_string()
.into()
};
chunks.push_back(
if !cfg!(debug_assertions) {
StreamChunk::Sync(
format!(
"<!>{}",
html_escape::encode_safe(&t.content)
content
)
.into(),
)
} else {
StreamChunk::Sync(html_escape::encode_safe(&t.content).to_string().into())
StreamChunk::Sync(content)
},
);
} else {
child.into_stream_chunks_helper(
cx, chunks,
cx,
chunks,
dont_escape_text,
);
}
}
@@ -435,7 +465,9 @@ impl View {
);
node.child
.into_stream_chunks_helper(
cx, chunks,
cx,
chunks,
dont_escape_text,
);
chunks.push_back(
StreamChunk::Sync(
@@ -447,6 +479,26 @@ impl View {
),
);
}
#[cfg(not(debug_assertions))]
{
node.child
.into_stream_chunks_helper(
cx,
chunks,
dont_escape_text,
);
chunks.push_back(
StreamChunk::Sync(
format!(
"<!--hk={}-->",
HydrationCtx::to_string(
&id, true
)
)
.into(),
),
);
}
}
},
)

View File

@@ -4,10 +4,12 @@ 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,
@@ -16,6 +18,7 @@ impl Transparent {
}
/// 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,
@@ -31,6 +34,7 @@ impl fmt::Debug for Transparent {
}
impl PartialEq for Transparent {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(&self.0, &other.0)
}
@@ -39,6 +43,7 @@ impl PartialEq for Transparent {
impl Eq for Transparent {}
impl IntoView for Transparent {
#[inline(always)]
fn into_view(self, _: Scope) -> View {
View::Transparent(self)
}

View File

@@ -26,6 +26,7 @@ leptos_hot_reload = { workspace = true }
server_fn_macro = { workspace = true }
convert_case = "0.6.0"
uuid = { version = "1", features = ["v4"] }
tracing = "0.1.37"
[dev-dependencies]
log = "0.4"

View File

@@ -91,7 +91,7 @@ impl Parse for Model {
// implemented manually because Vec::drain_filter is nightly only
// follows std recommended parallel
fn drain_filter<T>(
pub fn drain_filter<T>(
vec: &mut Vec<T>,
mut some_predicate: impl FnMut(&mut T) -> bool,
) {
@@ -105,7 +105,7 @@ fn drain_filter<T>(
}
}
fn convert_from_snake_case(name: &Ident) -> Ident {
pub fn convert_from_snake_case(name: &Ident) -> Ident {
let name_str = name.to_string();
if !name_str.is_case(Snake) {
name.clone()
@@ -157,8 +157,8 @@ impl ToTokens for Model {
quote! {
#[allow(clippy::let_with_type_underscore)]
#[cfg_attr(
debug_assertions,
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
any(debug_assertions, feature="ssr"),
::leptos::leptos_dom::tracing::instrument(level = "info", name = #trace_name, skip_all)
)]
},
quote! {
@@ -285,7 +285,7 @@ impl Prop {
}
#[derive(Clone)]
struct Docs(Vec<Attribute>);
pub struct Docs(Vec<Attribute>);
impl ToTokens for Docs {
fn to_tokens(&self, tokens: &mut TokenStream) {
@@ -300,7 +300,7 @@ impl ToTokens for Docs {
}
impl Docs {
fn new(attrs: &[Attribute]) -> Self {
pub fn new(attrs: &[Attribute]) -> Self {
let attrs = attrs
.iter()
.filter(|attr| attr.path == parse_quote!(doc))
@@ -310,7 +310,7 @@ impl Docs {
Self(attrs)
}
fn padded(&self) -> TokenStream {
pub fn padded(&self) -> TokenStream {
self.0
.iter()
.enumerate()
@@ -344,7 +344,7 @@ impl Docs {
.collect()
}
fn typed_builder(&self) -> String {
pub fn typed_builder(&self) -> String {
#[allow(unstable_name_collisions)]
let doc_str = self
.0
@@ -517,7 +517,7 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
}
}
fn is_option(ty: &Type) -> bool {
pub fn is_option(ty: &Type) -> bool {
if let Type::Path(TypePath {
path: Path { segments, .. },
..
@@ -533,7 +533,7 @@ fn is_option(ty: &Type) -> bool {
}
}
fn unwrap_option(ty: &Type) -> Type {
pub fn unwrap_option(ty: &Type) -> Type {
const STD_OPTION_MSG: &str =
"make sure you're not shadowing the `std::option::Option` type that \
is automatically imported from the standard prelude";

View File

@@ -5,7 +5,7 @@
extern crate proc_macro_error;
use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use proc_macro2::{Span, TokenTree};
use quote::ToTokens;
use server_fn_macro::{server_macro_impl, ServerContext};
use syn::parse_macro_input;
@@ -35,6 +35,7 @@ mod view;
use template::render_template;
use view::render_view;
mod component;
mod slot;
mod template;
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
@@ -282,6 +283,10 @@ mod template;
/// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro]
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn view(tokens: TokenStream) -> TokenStream {
let tokens: proc_macro2::TokenStream = tokens.into();
let mut tokens = tokens.into_iter();
@@ -644,6 +649,128 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
.into()
}
/// Annotates a struct so that it can be used with your Component as a `slot`.
///
/// The `#[slot]` macro allows you to annotate plain Rust struct as component slots and use them
/// within your Leptos [component](crate::component!) properties. The struct can contain any number
/// of fields. When you use the component somewhere else, the names of the slot fields are the
/// names of the properties you use in the [view](crate::view!) macro.
///
/// Heres how you would define and use a simple Leptos component which can accept a custom slot:
/// ```rust
/// # use leptos::*;
/// use std::time::Duration;
///
/// #[slot]
/// struct HelloSlot {
/// // Same prop syntax as components.
/// #[prop(optional)]
/// children: Option<Children>,
/// }
///
/// #[component]
/// fn HelloComponent(
/// cx: Scope,
/// /// 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)(cx).into_view(cx)
/// } else {
/// ().into_view(cx)
/// }
/// }
///
/// #[component]
/// fn App(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <HelloComponent>
/// <HelloSlot slot>
/// "Hello, World!"
/// </HelloSlot>
/// </HelloComponent>
/// }
/// }
/// ```
///
/// /// Here are some important details about how slots work within the framework:
/// 1. Most of the same rules from [component](crate::component!) macro should also be followed on slots.
///
/// 2. Specifying only `slot` without a name (such as in `<HelloSlot slot>`) will default the chosen slot to
/// the a snake case version of the slot struct name (`hello_slot` for `<HelloSlot>`).
///
/// 3. Event handlers cannot be specified directly on the slot.
///
/// ```compile_error
/// // ❌ This won't work
/// # use leptos::*;
///
/// #[slot]
/// struct SlotWithChildren {
/// children: Children,
/// }
///
/// #[component]
/// fn ComponentWithSlot(cx: Scope, slot: SlotWithChildren) -> impl IntoView {
/// (slot.children)(cx)
/// }
///
/// #[component]
/// fn App(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <ComponentWithSlot>
/// <SlotWithChildren slot:slot on:click=move |_| {}>
/// <h1>"Hello, World!"</h1>
/// </SlotWithChildren>
/// </ComponentWithSlot>
/// }
/// }
/// ```
///
/// ```
/// // ✅ Do this instead
/// # use leptos::*;
///
/// #[slot]
/// struct SlotWithChildren {
/// children: Children,
/// }
///
/// #[component]
/// fn ComponentWithSlot(cx: Scope, slot: SlotWithChildren) -> impl IntoView {
/// (slot.children)(cx)
/// }
///
/// #[component]
/// fn App(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <ComponentWithSlot>
/// <SlotWithChildren slot:slot>
/// <div on:click=move |_| {}>
/// <h1>"Hello, World!"</h1>
/// </div>
/// </SlotWithChildren>
/// </ComponentWithSlot>
/// }
/// }
/// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute]
pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
if !args.is_empty() {
abort!(
Span::call_site(),
"no arguments are supported";
help = "try just `#[slot]`"
);
}
parse_macro_input!(s as slot::Model)
.into_token_stream()
.into()
}
/// Declares that a function is a [server function](https://docs.rs/server_fn/latest/server_fn/index.html).
/// This means that its body will only run on the server, i.e., when the `ssr` feature is enabled.
///

346
leptos_macro/src/slot.rs Normal file
View File

@@ -0,0 +1,346 @@
use crate::component::{
convert_from_snake_case, drain_filter, is_option, unwrap_option, Docs,
};
use attribute_derive::Attribute as AttributeDerive;
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, TokenStreamExt};
use syn::{
parse::Parse, parse_quote, Field, ItemStruct, LitStr, Type, Visibility,
};
pub struct Model {
docs: Docs,
vis: Visibility,
name: Ident,
props: Vec<Prop>,
body: ItemStruct,
}
impl Parse for Model {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut item = ItemStruct::parse(input)?;
let docs = Docs::new(&item.attrs);
let props = item
.fields
.clone()
.into_iter()
.map(Prop::new)
.collect::<Vec<_>>();
// We need to remove the `#[doc = ""]` and `#[builder(_)]`
// attrs from the function signature
drain_filter(&mut item.attrs, |attr| {
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
});
item.fields.iter_mut().for_each(|arg| {
drain_filter(&mut arg.attrs, |attr| {
attr.path == parse_quote!(doc)
|| attr.path == parse_quote!(prop)
});
});
Ok(Self {
docs,
vis: item.vis.clone(),
name: convert_from_snake_case(&item.ident),
props,
body: item,
})
}
}
impl ToTokens for Model {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
docs,
vis,
name,
props,
body,
} = self;
let (_, generics, where_clause) = body.generics.split_for_impl();
let prop_builder_fields = prop_builder_fields(vis, props);
let prop_docs = generate_prop_docs(props);
let builder_name_doc = LitStr::new(
&format!("Props for the [`{name}`] slot."),
name.span(),
);
let output = quote! {
#[doc = #builder_name_doc]
#[doc = ""]
#docs
#prop_docs
#[derive(::leptos::typed_builder::TypedBuilder)]
#[builder(doc)]
#vis struct #name #generics #where_clause {
#prop_builder_fields
}
impl From<#name> for Vec<#name> {
fn from(value: #name) -> Self {
vec![value]
}
}
};
tokens.append_all(output)
}
}
struct Prop {
docs: Docs,
prop_opts: PropOpt,
name: Ident,
ty: Type,
}
impl Prop {
fn new(arg: Field) -> Self {
let prop_opts =
PropOpt::from_attributes(&arg.attrs).unwrap_or_else(|e| {
// TODO: replace with `.unwrap_or_abort()` once https://gitlab.com/CreepySkeleton/proc-macro-error/-/issues/17 is fixed
abort!(e.span(), e.to_string());
});
let name = if let Some(i) = arg.ident {
i
} else {
abort!(
arg.ident,
"only `prop: bool` style types are allowed within the \
`#[slot]` macro"
);
};
Self {
docs: Docs::new(&arg.attrs),
prop_opts,
name,
ty: arg.ty,
}
}
}
#[derive(Clone, Debug, AttributeDerive)]
#[attribute(ident = prop)]
struct PropOpt {
#[attribute(conflicts = [optional_no_strip, strip_option])]
pub optional: bool,
#[attribute(conflicts = [optional, strip_option])]
pub optional_no_strip: bool,
#[attribute(conflicts = [optional, optional_no_strip])]
pub strip_option: bool,
#[attribute(example = "5 * 10")]
pub default: Option<syn::Expr>,
pub into: bool,
}
struct TypedBuilderOpts {
default: bool,
default_with_value: Option<syn::Expr>,
strip_option: bool,
into: bool,
}
impl TypedBuilderOpts {
pub fn from_opts(opts: &PropOpt, is_ty_option: bool) -> Self {
Self {
default: opts.optional || opts.optional_no_strip,
default_with_value: opts.default.clone(),
strip_option: opts.strip_option || opts.optional && is_ty_option,
into: opts.into,
}
}
}
impl ToTokens for TypedBuilderOpts {
fn to_tokens(&self, tokens: &mut TokenStream) {
let default = if let Some(v) = &self.default_with_value {
let v = v.to_token_stream().to_string();
quote! { default_code=#v, }
} else if self.default {
quote! { default, }
} else {
quote! {}
};
let strip_option = if self.strip_option {
quote! { strip_option, }
} else {
quote! {}
};
let into = if self.into {
quote! { into, }
} else {
quote! {}
};
let setter = if !strip_option.is_empty() || !into.is_empty() {
quote! { setter(#strip_option #into) }
} else {
quote! {}
};
let output = quote! { #[builder(#default #setter)] };
tokens.append_all(output);
}
}
fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
props
.iter()
.map(|prop| {
let Prop {
docs,
name,
prop_opts,
ty,
} = prop;
let builder_attrs =
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
quote! {
#docs
#builder_docs
#builder_attrs
#vis #name: #ty,
}
})
.collect()
}
fn generate_prop_docs(props: &[Prop]) -> TokenStream {
let required_prop_docs = props
.iter()
.filter(|Prop { prop_opts, .. }| {
!(prop_opts.optional || prop_opts.optional_no_strip)
})
.map(|p| prop_to_doc(p, PropDocStyle::List))
.collect::<TokenStream>();
let optional_prop_docs = props
.iter()
.filter(|Prop { prop_opts, .. }| {
prop_opts.optional || prop_opts.optional_no_strip
})
.map(|p| prop_to_doc(p, PropDocStyle::List))
.collect::<TokenStream>();
let required_prop_docs = if !required_prop_docs.is_empty() {
quote! {
#[doc = "# Required Props"]
#required_prop_docs
}
} else {
quote! {}
};
let optional_prop_docs = if !optional_prop_docs.is_empty() {
quote! {
#[doc = "# Optional Props"]
#optional_prop_docs
}
} else {
quote! {}
};
quote! {
#required_prop_docs
#optional_prop_docs
}
}
#[derive(Clone, Copy)]
enum PropDocStyle {
List,
Inline,
}
fn prop_to_doc(
Prop {
docs,
name,
ty,
prop_opts,
}: &Prop,
style: PropDocStyle,
) -> TokenStream {
let ty = if (prop_opts.optional || prop_opts.strip_option) && is_option(ty)
{
unwrap_option(ty)
} else {
ty.to_owned()
};
let type_item: syn::Item = parse_quote! {
type SomeType = #ty;
};
let file = syn::File {
shebang: None,
attrs: vec![],
items: vec![type_item],
};
let pretty_ty = prettyplease::unparse(&file);
let pretty_ty = &pretty_ty[16..&pretty_ty.len() - 2];
match style {
PropDocStyle::List => {
let arg_ty_doc = LitStr::new(
&if !prop_opts.into {
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
} else {
format!(
"- **{}**: `impl`[`Into<{}>`]",
quote!(#name),
pretty_ty
)
},
name.span(),
);
let arg_user_docs = docs.padded();
quote! {
#[doc = #arg_ty_doc]
#arg_user_docs
}
}
PropDocStyle::Inline => {
let arg_ty_doc = LitStr::new(
&if !prop_opts.into {
format!(
"**{}**: [`{}`]{}",
quote!(#name),
pretty_ty,
docs.typed_builder()
)
} else {
format!(
"**{}**: `impl`[`Into<{}>`]{}",
quote!(#name),
pretty_ty,
docs.typed_builder()
)
},
name.span(),
);
quote! {
#[builder(setter(doc = #arg_ty_doc))]
}
}
}
}

View File

@@ -1,7 +1,9 @@
use crate::{attribute_value, Mode};
use convert_case::{Case::Snake, Casing};
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use std::collections::HashMap;
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
@@ -149,14 +151,16 @@ pub(crate) fn render_view(
global_class: Option<&TokenTree>,
call_site: Option<String>,
) -> TokenStream {
let empty = {
let span = Span::call_site();
quote_spanned! {
span => leptos::leptos_dom::Unit
}
};
if mode == Mode::Ssr {
match nodes.len() {
0 => {
let span = Span::call_site();
quote_spanned! {
span => leptos::leptos_dom::Unit
}
}
0 => empty,
1 => {
root_node_to_tokens_ssr(cx, &nodes[0], global_class, call_site)
}
@@ -170,28 +174,27 @@ pub(crate) fn render_view(
}
} else {
match nodes.len() {
0 => {
let span = Span::call_site();
quote_spanned! {
span => leptos::leptos_dom::Unit
}
}
0 => empty,
1 => node_to_tokens(
cx,
&nodes[0],
TagType::Unknown,
None,
global_class,
call_site,
),
)
.unwrap_or_default(),
_ => fragment_to_tokens(
cx,
Span::call_site(),
nodes,
true,
TagType::Unknown,
None,
global_class,
call_site,
),
)
.unwrap_or(empty),
}
}
}
@@ -226,6 +229,7 @@ fn root_node_to_tokens_ssr(
}
Node::Element(node) => {
root_element_to_tokens_ssr(cx, node, global_class, view_marker)
.unwrap_or_default()
}
}
}
@@ -263,9 +267,14 @@ fn root_element_to_tokens_ssr(
node: &NodeElement,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> TokenStream {
) -> Option<TokenStream> {
if is_component_node(node) {
component_to_tokens(cx, node, global_class)
if let Some(slot) = get_slot(node) {
slot_to_tokens(cx, node, slot, None, global_class);
None
} else {
Some(component_to_tokens(cx, node, global_class))
}
} else {
let mut exprs_for_compiler = Vec::<TokenStream>::new();
@@ -275,6 +284,7 @@ fn root_element_to_tokens_ssr(
element_to_tokens_ssr(
cx,
node,
None,
&mut template,
&mut holes,
&mut chunks,
@@ -348,12 +358,12 @@ fn root_element_to_tokens_ssr(
} else {
quote! {}
};
quote! {
Some(quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_chunks(#cx, #full_name, [#(#chunks),*])#view_marker
}
}
})
}
}
@@ -369,6 +379,7 @@ enum SsrElementChunks {
fn element_to_tokens_ssr(
cx: &Ident,
node: &NodeElement,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
template: &mut String,
holes: &mut Vec<TokenStream>,
chunks: &mut Vec<SsrElementChunks>,
@@ -377,13 +388,20 @@ fn element_to_tokens_ssr(
global_class: Option<&TokenTree>,
) {
if is_component_node(node) {
if let Some(slot) = get_slot(node) {
slot_to_tokens(cx, node, slot, parent_slots, global_class);
return;
}
let component = component_to_tokens(cx, node, global_class);
if !template.is_empty() {
chunks.push(SsrElementChunks::String {
template: std::mem::take(template),
holes: std::mem::take(holes),
})
}
chunks.push(SsrElementChunks::View(quote! {
{#component}.into_view(#cx)
}));
@@ -393,6 +411,7 @@ fn element_to_tokens_ssr(
.to_string()
.replace("svg::", "")
.replace("math::", "");
let is_script_or_style = tag_name == "script" || tag_name == "style";
template.push('<');
template.push_str(&tag_name);
@@ -406,6 +425,7 @@ fn element_to_tokens_ssr(
template,
holes,
exprs_for_compiler,
global_class,
);
}
}
@@ -451,6 +471,7 @@ fn element_to_tokens_ssr(
element_to_tokens_ssr(
cx,
child,
None,
template,
holes,
chunks,
@@ -461,9 +482,16 @@ fn element_to_tokens_ssr(
}
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&html_escape::encode_safe(
&value,
));
let value = if is_script_or_style {
value.into()
} else {
html_escape::encode_safe(&value)
};
template.push_str(
&value
.replace('{', "{{")
.replace('}', "}}"),
);
} else {
template.push_str("{}");
let value = text.value.as_ref();
@@ -513,14 +541,17 @@ fn attribute_to_tokens_ssr<'a>(
template: &mut String,
holes: &mut Vec<TokenStream>,
exprs_for_compiler: &mut Vec<TokenStream>,
global_class: Option<&TokenTree>,
) -> Option<&'a NodeValueExpr> {
let name = node.key.to_string();
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
// ignore refs on SSR
} else if name.strip_prefix("on:").is_some() {
let (event_type, handler) = event_from_attribute_node(node, false);
} else if let Some(name) = name.strip_prefix("on:") {
let handler = attribute_value(node);
let (event_type, _, _) = parse_event_name(name);
exprs_for_compiler.push(quote! {
leptos::leptos_dom::helpers::ssr_event_listener(#event_type, #handler);
leptos::leptos_dom::helpers::ssr_event_listener(::leptos::ev::#event_type, #handler);
})
} else if name.strip_prefix("prop:").is_some()
|| name.strip_prefix("class:").is_some()
@@ -532,6 +563,18 @@ fn attribute_to_tokens_ssr<'a>(
} else {
let name = name.replacen("attr:", "", 1);
// special case of global_class and class attribute
if name == "class"
&& global_class.is_some()
&& node.value.as_ref().and_then(value_to_string).is_none()
{
let span = node.key.span();
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, 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")
};
if name != "class" {
template.push(' ');
@@ -691,22 +734,50 @@ fn set_class_attribute_ssr(
}
}
#[allow(clippy::too_many_arguments)]
fn fragment_to_tokens(
cx: &Ident,
_span: Span,
nodes: &[Node],
lazy: bool,
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = node_to_tokens(cx, node, parent_type, global_class, None);
) -> Option<TokenStream> {
let mut slots = HashMap::new();
let has_slots = parent_slots.is_some();
quote! {
#node.into_view(#cx)
let mut nodes = nodes
.iter()
.filter_map(|node| {
let node = node_to_tokens(
cx,
node,
parent_type,
has_slots.then_some(&mut slots),
global_class,
None,
)?;
Some(quote! {
#node.into_view(#cx)
})
})
.peekable();
if nodes.peek().is_none() {
_ = nodes.collect::<Vec<_>>();
if let Some(parent_slots) = parent_slots {
for (slot, mut values) in slots.drain() {
parent_slots
.entry(slot)
.and_modify(|entry| entry.append(&mut values))
.or_insert(values);
}
}
});
return None;
}
let view_marker = if let Some(marker) = view_marker {
quote! { .with_view_marker(#marker) }
@@ -714,7 +785,7 @@ fn fragment_to_tokens(
quote! {}
};
if lazy {
let tokens = if lazy {
quote! {
{
leptos::Fragment::lazy(|| vec![
@@ -732,16 +803,28 @@ fn fragment_to_tokens(
#view_marker
}
}
};
if let Some(parent_slots) = parent_slots {
for (slot, mut values) in slots.drain() {
parent_slots
.entry(slot)
.and_modify(|entry| entry.append(&mut values))
.or_insert(values);
}
}
Some(tokens)
}
fn node_to_tokens(
cx: &Ident,
node: &Node,
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> TokenStream {
) -> Option<TokenStream> {
match node {
Node::Fragment(fragment) => fragment_to_tokens(
cx,
@@ -749,24 +832,32 @@ fn node_to_tokens(
&fragment.children,
true,
parent_type,
None,
global_class,
view_marker,
),
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Comment(_) | Node::Doctype(_) => Some(quote! {}),
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
Some(quote! {
leptos::leptos_dom::html::text(#value)
}
})
}
Node::Block(node) => {
let value = node.value.as_ref();
quote! { #value }
Some(quote! { #value })
}
Node::Attribute(node) => attribute_to_tokens(cx, node),
Node::Element(node) => {
element_to_tokens(cx, node, parent_type, global_class, view_marker)
Node::Attribute(node) => {
Some(attribute_to_tokens(cx, node, global_class))
}
Node::Element(node) => element_to_tokens(
cx,
node,
parent_type,
parent_slots,
global_class,
view_marker,
),
}
}
@@ -774,11 +865,17 @@ fn element_to_tokens(
cx: &Ident,
node: &NodeElement,
mut parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> TokenStream {
) -> Option<TokenStream> {
if is_component_node(node) {
component_to_tokens(cx, node, global_class)
if let Some(slot) = get_slot(node) {
slot_to_tokens(cx, node, slot, parent_slots, global_class);
None
} else {
Some(component_to_tokens(cx, node, global_class))
}
} else {
let tag = node.name.to_string();
let name = if is_custom_element(&tag) {
@@ -818,10 +915,13 @@ fn element_to_tokens(
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
if node.key.to_string().trim().starts_with("class:") {
let name = node.key.to_string();
if name.trim().starts_with("class:")
|| fancy_class_name(&name, cx, node).is_some()
{
None
} else {
Some(attribute_to_tokens(cx, node))
Some(attribute_to_tokens(cx, node, global_class))
}
} else {
None
@@ -829,8 +929,11 @@ fn element_to_tokens(
});
let class_attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
if node.key.to_string().trim().starts_with("class:") {
Some(attribute_to_tokens(cx, node))
let name = node.key.to_string();
if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) {
Some(fancy)
} else if name.trim().starts_with("class:") {
Some(attribute_to_tokens(cx, node, global_class))
} else {
None
}
@@ -850,37 +953,76 @@ fn element_to_tokens(
}
};
let children = node.children.iter().map(|node| {
let child = match node {
Node::Fragment(fragment) => fragment_to_tokens(
cx,
Span::call_site(),
&fragment.children,
true,
parent_type,
global_class,
None,
let (child, is_static) = match node {
Node::Fragment(fragment) => (
fragment_to_tokens(
cx,
Span::call_site(),
&fragment.children,
true,
parent_type,
None,
global_class,
None,
)
.unwrap_or({
let span = Span::call_site();
quote_spanned! {
span => leptos::leptos_dom::Unit
}
}),
false,
),
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
#[allow(unused_braces)] #value
if let Some(primitive) = value_to_string(&node.value) {
(quote! { #primitive }, true)
} else {
let value = node.value.as_ref();
(
quote! {
#[allow(unused_braces)] #value
},
false,
)
}
}
Node::Block(node) => {
let value = node.value.as_ref();
quote! {
#[allow(unused_braces)] #value
if let Some(primitive) = value_to_string(&node.value) {
(quote! { #primitive }, true)
} else {
let value = node.value.as_ref();
(
quote! {
#[allow(unused_braces)] #value
},
false,
)
}
}
Node::Element(node) => {
element_to_tokens(cx, node, parent_type, global_class, None)
}
Node::Element(node) => (
element_to_tokens(
cx,
node,
parent_type,
None,
global_class,
None,
)
.unwrap_or_default(),
false,
),
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => {
quote! {}
(quote! {}, false)
}
};
quote! {
.child((#cx, #child))
if is_static {
quote! {
.child(#child)
}
} else {
quote! {
.child((#cx, #child))
}
}
});
let view_marker = if let Some(marker) = view_marker {
@@ -888,18 +1030,22 @@ fn element_to_tokens(
} else {
quote! {}
};
quote! {
Some(quote! {
#name
#(#attrs)*
#(#class_attrs)*
#global_class_expr
#(#children)*
#view_marker
}
})
}
}
fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
fn attribute_to_tokens(
cx: &Ident,
node: &NodeAttribute,
global_class: Option<&TokenTree>,
) -> TokenStream {
let span = node.key.span();
let name = node.key.to_string();
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
@@ -911,24 +1057,9 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
} else if let Some(name) = name.strip_prefix("on:") {
let handler = attribute_value(node);
let (name, is_force_undelegated) = parse_event(name);
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let is_custom = event_type == "Custom";
let Ok(event_type) = event_type.parse::<TokenStream>() else {
abort!(event_type, "couldn't parse event name");
};
let event_type = if is_custom {
quote! { Custom::new(#name) }
} else {
event_type
};
let (event_type, is_custom, is_force_undelegated) =
parse_event_name(name);
let event_name_ident = match &node.key {
NodeName::Punctuated(parts) => {
@@ -1025,6 +1156,18 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
return fancy;
}
// special case of global_class and class attribute
if name == "class"
&& global_class.is_some()
&& node.value.as_ref().and_then(value_to_string).is_none()
{
let span = node.key.span();
proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, 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")
};
// all other attributes
let value = match node.value.as_ref() {
Some(value) => {
@@ -1034,6 +1177,7 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
None => quote_spanned! { span => "" },
};
let attr = match &node.key {
NodeName::Punctuated(parts) => Some(&parts[0]),
_ => None,
@@ -1054,6 +1198,163 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
}
pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
let (name, is_force_undelegated) = parse_event(name);
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let is_custom = event_type == "Custom";
let Ok(event_type) = event_type.parse::<TokenStream>() else {
abort!(event_type, "couldn't parse event name");
};
let event_type = if is_custom {
quote! { Custom::new(#name) }
} else {
event_type
};
(event_type, is_custom, is_force_undelegated)
}
pub(crate) fn slot_to_tokens(
cx: &Ident,
node: &NodeElement,
slot: &NodeAttribute,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
) {
let name = slot.key.to_string();
let name = name.trim();
let name = convert_to_snake_case(if name.starts_with("slot:") {
name.replacen("slot:", "", 1)
} else {
node.name.to_string()
});
let component_name = ident_from_tag_name(&node.name);
let span = node.name.span();
let Some(parent_slots) = parent_slots else {
proc_macro_error::emit_error!(span, "slots cannot be used inside HTML elements");
return;
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
if is_slot(node) {
None
} else {
Some(node)
}
} else {
None
}
});
let props = attrs
.clone()
.filter(|attr| !attr.key.to_string().starts_with("clone:"))
.map(|attr| {
let name = &attr.key;
let value = attr
.value
.as_ref()
.map(|v| {
let v = v.as_ref();
quote! { #v }
})
.unwrap_or_else(|| quote! { #name });
quote! {
.#name(#[allow(unused_braces)] #value)
}
});
let items_to_clone = attrs
.clone()
.filter_map(|attr| {
attr.key
.to_string()
.strip_prefix("clone:")
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
})
.collect::<Vec<_>>();
let mut slots = HashMap::new();
let children = if node.children.is_empty() {
quote! {}
} else {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let marker = format!("<{component_name}/>-children");
let view_marker = quote! { .with_view_marker(#marker) };
} else {
let view_marker = quote! {};
}
}
let children = fragment_to_tokens(
cx,
span,
&node.children,
true,
TagType::Unknown,
Some(&mut slots),
global_class,
None,
);
if let Some(children) = children {
let clonables = items_to_clone
.iter()
.map(|ident| quote! { let #ident = #ident.clone(); });
quote! {
.children({
#(#clonables)*
Box::new(move |#cx| #children #view_marker)
})
}
} else {
quote! {}
}
};
let slots = slots.drain().map(|(slot, values)| {
let slot = Ident::new(&slot, span);
if values.len() > 1 {
quote! {
.#slot(vec![
#(#values)*
])
}
} else {
let value = &values[0];
quote! { .#slot(#value) }
}
});
let slot = quote! {
#component_name::builder()
#(#props)*
#(#slots)*
#children
.build()
.into(),
};
parent_slots
.entry(name)
.and_modify(|entry| entry.push(slot.clone()))
.or_insert(vec![slot]);
}
pub(crate) fn component_to_tokens(
cx: &Ident,
node: &NodeElement,
@@ -1115,6 +1416,7 @@ pub(crate) fn component_to_tokens(
})
.collect::<Vec<_>>();
let mut slots = HashMap::new();
let children = if node.children.is_empty() {
quote! {}
} else {
@@ -1133,28 +1435,48 @@ pub(crate) fn component_to_tokens(
&node.children,
true,
TagType::Unknown,
Some(&mut slots),
global_class,
None,
);
let clonables = items_to_clone
.iter()
.map(|ident| quote! { let #ident = #ident.clone(); });
if let Some(children) = children {
let clonables = items_to_clone
.iter()
.map(|ident| quote! { let #ident = #ident.clone(); });
quote! {
.children({
#(#clonables)*
quote! {
.children({
#(#clonables)*
Box::new(move |#cx| #children #view_marker)
})
Box::new(move |#cx| #children #view_marker)
})
}
} else {
quote! {}
}
};
let slots = slots.drain().map(|(slot, values)| {
let slot = Ident::new(&slot, span);
if values.len() > 1 {
quote! {
.#slot(vec![
#(#values)*
])
}
} else {
let value = &values[0];
quote! { .#slot(#value) }
}
});
let component = quote! {
#name(
#cx,
::leptos::component_props_builder(&#name)
#(#props)*
#(#slots)*
#children
.build()
)
@@ -1242,6 +1564,34 @@ fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
}
}
fn is_slot(node: &NodeAttribute) -> bool {
let key = node.key.to_string();
let key = key.trim();
key == "slot" || key.starts_with("slot:")
}
fn get_slot(node: &NodeElement) -> Option<&NodeAttribute> {
node.attributes.iter().find_map(|node| {
if let Node::Attribute(node) = node {
if is_slot(node) {
Some(node)
} else {
None
}
} else {
None
}
})
}
fn convert_to_snake_case(name: String) -> String {
if !name.is_case(Snake) {
name.to_case(Snake)
} else {
name
}
}
fn is_custom_element(tag: &str) -> bool {
tag.contains('-')
}

View File

@@ -3,8 +3,8 @@
use crate::{runtime::with_runtime, Scope};
use std::any::{Any, TypeId};
/// Provides a context value of type `T` to the current reactive [Scope](crate::Scope)
/// and all of its descendants. This can be consumed using [use_context](crate::use_context).
/// Provides a context value of type `T` to the current reactive [`Scope`](crate::Scope)
/// and all of its descendants. This can be consumed using [`use_context`](crate::use_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
@@ -27,7 +27,7 @@ use std::any::{Any, TypeId};
/// #[component]
/// pub fn Provider(cx: Scope) -> impl IntoView {
/// let (value, set_value) = create_signal(cx, 0);
///
///
/// // the newtype pattern isn't *necessary* here but is a good practice
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
/// // and makes it easier to refer to it in ButtonD
@@ -47,6 +47,10 @@ use std::any::{Any, TypeId};
/// todo!()
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", skip_all,)
)]
pub fn provide_context<T>(cx: Scope, value: T)
where
T: Clone + 'static,
@@ -61,9 +65,9 @@ where
}
/// Extracts a context value of type `T` from the reactive system by traversing
/// it upwards, beginning from the current [Scope](crate::Scope) and iterating
/// it upwards, beginning from the current [`Scope`](crate::Scope) and iterating
/// through its parents, if any. The context value should have been provided elsewhere
/// using [provide_context](crate::provide_context).
/// using [`provide_context`](crate::provide_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
@@ -86,7 +90,7 @@ where
/// #[component]
/// pub fn Provider(cx: Scope) -> impl IntoView {
/// let (value, set_value) = create_signal(cx, 0);
///
///
/// // the newtype pattern isn't *necessary* here but is a good practice
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
/// // and makes it easier to refer to it in ButtonD
@@ -106,6 +110,10 @@ where
/// todo!()
/// }
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", skip_all,)
)]
pub fn use_context<T>(cx: Scope) -> Option<T>
where
T: Clone + 'static,
@@ -140,3 +148,61 @@ where
.ok()
.flatten()
}
/// Extracts a context value of type `T` from the reactive system by traversing
/// it upwards, beginning from the current [Scope](crate::Scope) and iterating
/// through its parents, if any. The context value should have been provided elsewhere
/// using [provide_context](crate::provide_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
/// arguments to a function or properties of a component.
///
/// Context works similarly to variable scope: a context that is provided higher in
/// the component tree can be used lower down, but a context that is provided lower
/// in the tree cannot be used higher up.
///
/// ```
/// use leptos::*;
///
/// // define a newtype we'll provide as context
/// // contexts are stored by their types, so it can be useful to create
/// // a new type to avoid confusion with other `WriteSignal<i32>`s we may have
/// // all types to be shared via context should implement `Clone`
/// #[derive(Copy, Clone)]
/// struct ValueSetter(WriteSignal<i32>);
///
/// #[component]
/// pub fn Provider(cx: Scope) -> impl IntoView {
/// let (value, set_value) = create_signal(cx, 0);
///
/// // the newtype pattern isn't *necessary* here but is a good practice
/// // it avoids confusion with other possible future `WriteSignal<bool>` contexts
/// // and makes it easier to refer to it in ButtonD
/// provide_context(cx, ValueSetter(set_value));
///
/// // because <Consumer/> is nested inside <Provider/>,
/// // it has access to the provided context
/// view! { cx, <div><Consumer/></div> }
/// }
///
/// #[component]
/// pub fn Consumer(cx: Scope) -> impl IntoView {
/// // consume the provided context of type `ValueSetter` using `use_context`
/// // this traverses up the tree of `Scope`s and gets the nearest provided `ValueSetter`
/// let set_value = expect_context::<ValueSetter>(cx).0;
///
/// todo!()
/// }
/// ```
pub fn expect_context<T>(cx: Scope) -> T
where
T: Clone + 'static,
{
use_context(cx).unwrap_or_else(|| {
panic!(
"context of type {:?} to be present",
std::any::type_name::<T>()
)
})
}

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