mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
93 Commits
root-redir
...
mutually-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1adddce0ed | ||
|
|
c8e5d0518a | ||
|
|
c334c576ab | ||
|
|
e14b970291 | ||
|
|
7a5a776cb9 | ||
|
|
06f782aa13 | ||
|
|
6b825fec37 | ||
|
|
b452d8af40 | ||
|
|
e96f1d2129 | ||
|
|
72d6af9c84 | ||
|
|
8198cd0b68 | ||
|
|
fe68b47ba2 | ||
|
|
384d39543c | ||
|
|
225e62d12f | ||
|
|
3905a2aa60 | ||
|
|
ff6ce2dac0 | ||
|
|
16675cbff2 | ||
|
|
9524c6e289 | ||
|
|
bc316c648c | ||
|
|
6753ba21c4 | ||
|
|
efbe32e081 | ||
|
|
55fd6d44f9 | ||
|
|
90972f2d94 | ||
|
|
7382c7e51c | ||
|
|
8a6d129575 | ||
|
|
e20c77710d | ||
|
|
93da88eac0 | ||
|
|
5072539917 | ||
|
|
78c59df1d1 | ||
|
|
75e40eafb2 | ||
|
|
274a1ac5f0 | ||
|
|
17040a4af4 | ||
|
|
b09a5f905e | ||
|
|
683511f311 | ||
|
|
151c58733b | ||
|
|
012ff56cd6 | ||
|
|
493c805993 | ||
|
|
764192af36 | ||
|
|
f969fd7eff | ||
|
|
2c7ee0d415 | ||
|
|
5430c78e18 | ||
|
|
6b052557d1 | ||
|
|
70f3edb0f5 | ||
|
|
4e1f963750 | ||
|
|
3c3d3b33f1 | ||
|
|
be7b9eea25 | ||
|
|
016ad6b7a6 | ||
|
|
5dab35447a | ||
|
|
63be819533 | ||
|
|
af8afb1204 | ||
|
|
2170be8e01 | ||
|
|
1187a506dd | ||
|
|
ff5ceddbe2 | ||
|
|
41a5e09caa | ||
|
|
60b96c9118 | ||
|
|
7ccb2d9f44 | ||
|
|
2c2090a194 | ||
|
|
de9b2998ac | ||
|
|
29b81a3d50 | ||
|
|
5bc0d89ce7 | ||
|
|
342b10c232 | ||
|
|
ba9d3c1602 | ||
|
|
d3b3ce6980 | ||
|
|
4b79a91287 | ||
|
|
de06c9b2ca | ||
|
|
84c7d00ea9 | ||
|
|
8f5ae0054d | ||
|
|
374f0c4e27 | ||
|
|
a6170f4da9 | ||
|
|
578dd5ef35 | ||
|
|
934a131deb | ||
|
|
5bc1c36e67 | ||
|
|
b1b9853f92 | ||
|
|
5d6a083d1d | ||
|
|
9478245986 | ||
|
|
4c1c12734a | ||
|
|
5d3a360456 | ||
|
|
b51da35a9a | ||
|
|
164dcd1b97 | ||
|
|
c0964c2b01 | ||
|
|
af5b226e53 | ||
|
|
4e7a0db950 | ||
|
|
cee6ed9a9f | ||
|
|
fa1013f7c3 | ||
|
|
3a1db3a191 | ||
|
|
8b57ba7aa8 | ||
|
|
ea638e37f6 | ||
|
|
4342d45a2f | ||
|
|
fe4d2382b8 | ||
|
|
2a13609eff | ||
|
|
c2ff1cabf1 | ||
|
|
54370e3153 | ||
|
|
e72ed26809 |
2
.github/workflows/check-stable.yml
vendored
2
.github/workflows/check-stable.yml
vendored
@@ -42,4 +42,4 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make check-stable
|
||||
run: cargo make --profile=github-actions check-stable
|
||||
|
||||
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -42,4 +42,4 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all libraries
|
||||
run: cargo make check
|
||||
run: cargo make --profile=github-actions check
|
||||
|
||||
37
.github/workflows/publish-book.yml
vendored
Normal file
37
.github/workflows/publish-book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Deploy book
|
||||
on:
|
||||
push:
|
||||
paths: ['docs/book/**']
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pull-requests: write # To create a PR from that branch
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mdbook
|
||||
run: |
|
||||
mkdir mdbook
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.27/mdbook-v0.4.27-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Deploy GitHub Pages
|
||||
run: |
|
||||
cd docs/book
|
||||
mdbook build
|
||||
git worktree add gh-pages
|
||||
git config user.name "Deploy book from CI"
|
||||
git config user.email ""
|
||||
cd gh-pages
|
||||
# Delete the ref to avoid keeping history.
|
||||
git update-ref -d refs/heads/gh-pages
|
||||
rm -rf *
|
||||
mv ../book/* .
|
||||
git add .
|
||||
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
|
||||
git push --force --set-upstream origin gh-pages
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -42,4 +42,4 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make test
|
||||
run: cargo make --profile=github-actions test
|
||||
|
||||
@@ -39,7 +39,7 @@ server_fn_macro = { path = "./server_fn_macro", default-features = false, versio
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
|
||||
leptos_router = { path = "./router", version = "0.2.5" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.5" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.2.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -74,3 +74,9 @@ dependencies = ["test-all"]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[env]
|
||||
RUSTFLAGS=""
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS="-D warnings"
|
||||
44
README.md
44
README.md
@@ -6,6 +6,7 @@
|
||||
[](https://crates.io/crates/leptos)
|
||||
[](https://docs.rs/leptos)
|
||||
[](https://discord.gg/YdRAhS7eQB)
|
||||
[](https://matrix.to/#/#leptos:matrix.org)
|
||||
|
||||
# Leptos
|
||||
|
||||
@@ -24,8 +25,7 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// create user interfaces with the declarative `view!` macro
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
@@ -48,27 +48,27 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
|
||||
## What does that mean?
|
||||
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)
|
||||
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
|
||||
|
||||
## Learn more
|
||||
|
||||
Here are some resources for learning more about Leptos:
|
||||
|
||||
- [Book](https://leptos-rs.github.io/leptos/) (work in progress)
|
||||
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- Leptos Guide (in progress)
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` Rust.
|
||||
Most of the examples assume you’re 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 haven’t already)
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
```
|
||||
rustup toolchain install nightly
|
||||
@@ -76,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 you’re on `stable`, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
|
||||
@@ -86,7 +94,7 @@ If you’re on `stable`, note the following:
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
@@ -95,13 +103,13 @@ cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
Open browser to [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
## FAQs
|
||||
|
||||
### What’s up with the name?
|
||||
|
||||
*Leptos* (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
@@ -109,7 +117,7 @@ People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
With 0.1 the APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
|
||||
The APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
@@ -119,7 +127,7 @@ Yes, I’m sure there are. You can see from the state of our issue tracker over
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
@@ -137,8 +145,8 @@ I've put together a [very simple GTK example](https://github.com/leptos-rs/lepto
|
||||
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
|
||||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. (Dioxus has made huge advances in performance with its recent 0.3 release, and is now roughly on par with Leptos.)
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. You can call functions, create timeouts, etc. within the body of your component functions because they won’t be re-run. You don’t need to think about manual dependency tracking for effects; fine-grained reactivity tracks dependencies automatically.
|
||||
|
||||
### How is this different from Sycamore?
|
||||
|
||||
@@ -146,9 +154,9 @@ Conceptually, these two frameworks are very similar: because both are built on f
|
||||
|
||||
There are some practical differences that make a significant difference:
|
||||
|
||||
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
|
||||
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
```rust
|
||||
|
||||
@@ -17,15 +17,11 @@ lazy_static = "1"
|
||||
log = "0.4"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
serde = { version = "1", features = ["derive", "rc"]}
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
reactive-signals = "0.1.0-alpha.4"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Window",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement"
|
||||
]
|
||||
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
extern crate test;
|
||||
|
||||
mod reactive;
|
||||
//åmod reactive;
|
||||
//mod ssr;
|
||||
//mod todomvc;
|
||||
mod todomvc;
|
||||
|
||||
@@ -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));
|
||||
@@ -162,6 +161,77 @@ fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_deep_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
|
||||
let sc = ClientRuntime::new_root_scope();
|
||||
b.iter(|| {
|
||||
let signal = signal!(sc, 0);
|
||||
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(signal!(sc, move || prev.get() + 1))
|
||||
} else {
|
||||
memos.push(signal!(sc, move || signal.get() + 1))
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_fanning_out(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
let sig = signal!(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| signal!(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_narrowing_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
|
||||
let memo = signal!(cx, {
|
||||
let sigs = sigs.clone();
|
||||
move || {
|
||||
sigs.iter().map(|r| r.get()).sum::<i32>()
|
||||
}
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
signal!(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move || {
|
||||
acc.set(memo.get());
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
sigs[1].update(|n| *n += 1);
|
||||
sigs[10].update(|n| *n += 1);
|
||||
sigs[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo.get(), 499503);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_creation(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
@@ -171,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));
|
||||
@@ -195,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));
|
||||
@@ -373,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));
|
||||
@@ -394,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));
|
||||
|
||||
@@ -4,7 +4,7 @@ use test::Bencher;
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use leptos::*;
|
||||
HydrationCtx::reset_id();
|
||||
leptos_dom::HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
|
||||
@@ -32,7 +32,8 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use leptos::*;
|
||||
use miniserde::*;
|
||||
use web_sys::HtmlInputElement;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
@@ -110,10 +111,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
provide_context(cx, set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
@@ -167,57 +164,79 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" on:keydown=add_todo />
|
||||
</header>
|
||||
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |todo: Todo| view! { cx, <Todo todo=todo.clone() /> }
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus=""
|
||||
on:keydown=add_todo
|
||||
/>
|
||||
</header>
|
||||
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
type="checkbox"
|
||||
prop:checked=move || todos.with(|t| t.remaining() > 0)
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |cx, todo: Todo| {
|
||||
view! { cx, <Todo todo=todo.clone()/> }
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a
|
||||
href="#/"
|
||||
class="selected"
|
||||
class:selected=move || mode() == Mode::All
|
||||
>
|
||||
"All"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/active" class:selected=move || mode() == Mode::Active>
|
||||
"Active"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
|
||||
"Completed"
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden=move || todos.with(|t| t.completed() == 0)
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -237,41 +256,36 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
//_ref=input
|
||||
>
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
/>
|
||||
<label on:dblclick=move |_| set_editing(true)>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
|
||||
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
|
||||
<button
|
||||
class="destroy"
|
||||
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
|
||||
></button>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
prop:value=move || todo.title.get()
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup=move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,15 @@ mod yew;
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
}
|
||||
.into_view(cx)
|
||||
.render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,21 +53,20 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
/*
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
}
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,5 +103,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) {
|
||||
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -107,4 +107,6 @@ create_effect(cx, move |prev_value| {
|
||||
|
||||
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -169,4 +169,6 @@ somewhere else that only takes `state.name`, clicking the button won’t cause
|
||||
that other slice to update. This allows you to combine the benefits of a top-down
|
||||
data flow and of fine-grained reactive updates.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px">
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh">
|
||||
|
||||
@@ -27,11 +27,12 @@
|
||||
- [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]()
|
||||
- [Server Side Rendering](./ssr/README.md)
|
||||
- [`cargo-leptos`]()
|
||||
- [The Life of a Page Load](./ssr/21_life_cycle.md)
|
||||
- [Async Rendering and SSR “Modes”](./ssr/22_ssr_modes.md)
|
||||
- [Hydration Footguns]()
|
||||
- [Request/Response]()
|
||||
- [Extractors]()
|
||||
|
||||
@@ -50,4 +50,6 @@ view! { cx,
|
||||
|
||||
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
}
|
||||
```
|
||||
@@ -69,4 +69,6 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
|
||||
|
||||
This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -6,4 +6,6 @@ You’ll notice in the `<Suspense/>` example that if you keep reloading the data
|
||||
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -91,4 +91,6 @@ view! { cx,
|
||||
|
||||
Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
112
docs/book/src/interlude_styling.md
Normal file
112
docs/book/src/interlude_styling.md
Normal 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) don’t 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 Tailwind’s 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 doesn’t 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 no opinions on how you style your website or app, but we’re very happy to provide support to any tools you’re trying to create to make it easier. If you’re working on a CSS or styling approach that you’d like to add to this list, please let us know!
|
||||
@@ -167,4 +167,6 @@ In fact, in this case, we don’t even need to rerender the `<Contact/>` compone
|
||||
|
||||
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -74,4 +74,6 @@ This can get a little messy: deriving a signal that wraps an `Option<_>` or `Res
|
||||
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explain them all yet.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -18,4 +18,6 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_
|
||||
|
||||
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -64,4 +64,6 @@ view! { cx,
|
||||
|
||||
You’ll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the user’s input as they type.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
43
docs/book/src/ssr/21_life_cycle.md
Normal file
43
docs/book/src/ssr/21_life_cycle.md
Normal 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?
|
||||
|
||||
I’m assuming some basic knowledge of how the Internet works here, and won’t get into the weeds about HTTP or whatever. Instead, I’ll 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 that’s 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 server’s 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 that’s 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. (There’s 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 that’s returned is essentially your app, “dehydrated” or “freeze-dried”: it is HTML without any of the reactivity or event listeners you’ve 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 doesn’t 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 won’t 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. We’ll 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 can’t I just `.await` this on the server? If I can just call library X in a server function, why can’t 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 it’s the most common way, and we happen to think it’s quite a good way, to create the smoothest possible experience for your users.
|
||||
122
docs/book/src/ssr/22_ssr_modes.md
Normal file
122
docs/book/src/ssr/22_ssr_modes.md
Normal 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 doesn’t 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? (Let’s 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? (Let’s call this “synchronous” rendering) Or is there some middle-ground solution that somehow beats them both? (Hint: There is.)
|
||||
|
||||
If you’ve ever listened to streaming music or watched a video online, I’m 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 there’s 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 you’re 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 you’re 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 you’ve 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/>`.
|
||||
- Can’t 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 it’s 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>
|
||||
// We’ll 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 doesn’t 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 there’s 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 post’s title and metadata in the initial HTML `<head>`. But I really don’t care whether comments have loaded yet or not; I’d 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.
|
||||
21
docs/book/src/ssr/README.md
Normal file
21
docs/book/src/ssr/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Server Side Rendering
|
||||
|
||||
So far, everything we’ve written has been rendered almost entirely in the browser. When we create an app using Trunk, it’s served using a local development server. If you build it for production and deploy it, it’s served by whatever server or CDN you’re using. In either case, what’s 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 user’s screen is blank until additional resources have been downloaded.
|
||||
2. It’s bad for SEO, as load times are longer and the HTML you serve has no meaningful content.
|
||||
3. It’s broken for users for whom JS/WASM don’t load for some reason (e.g., they’re on a train and just went into a tunnel before WASM finished loading; they’re using an older device that doesn’t 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, it’s 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!
|
||||
@@ -1,14 +1,14 @@
|
||||
# A Basic Component
|
||||
|
||||
That “Hello, world!” was a *very* simple example. Let’s move on to something a
|
||||
That “Hello, world!” was a _very_ simple example. Let’s move on to something a
|
||||
little more like an ordinary app.
|
||||
|
||||
First, let’s edit the `main` function so that, instead of rendering the whole
|
||||
app, it just renders an `<App/>` component. Components are the basic unit of
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
`<App/>` component.
|
||||
|
||||
```rust
|
||||
@@ -39,11 +39,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
```
|
||||
|
||||
## The Component Signature
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
```
|
||||
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
used as a component in your Leptos application. We’ll see some of the other features of
|
||||
this macro in a couple chapters.
|
||||
|
||||
@@ -52,6 +53,7 @@ fn App(cx: Scope) -> impl IntoView
|
||||
```
|
||||
|
||||
Every component is a function with the following characteristics
|
||||
|
||||
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
as its first argument. This `Scope` is our entrypoint into the reactive system.
|
||||
By convention, it’s usually named `cx`.
|
||||
@@ -60,7 +62,8 @@ Every component is a function with the following characteristics
|
||||
anything you could return from a Leptos `view`.
|
||||
|
||||
## The Component Body
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
render function that reruns multiple times. You’ll typically use it to create a
|
||||
few reactive variables, define any side effects that run in response to those values
|
||||
changing, and describe the user interface.
|
||||
@@ -68,16 +71,17 @@ changing, and describe the user interface.
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
```
|
||||
|
||||
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
|
||||
creates a signal, the basic unit of reactive change and state management in Leptos.
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
current value, you’ll call `set_count.set(...)` (or `set_count(...)`).
|
||||
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
> efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
|
||||
## The View
|
||||
## The View
|
||||
|
||||
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
|
||||
|
||||
@@ -100,25 +104,28 @@ view! { cx,
|
||||
This should mostly be easy to understand: it looks like HTML, with a special
|
||||
`on:click` to define a `click` event listener, a text node that’s formatted like
|
||||
a Rust string, and then...
|
||||
|
||||
```rust
|
||||
{move || count.get()}
|
||||
```
|
||||
|
||||
whatever that is.
|
||||
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
into the view tells the framework: “Hey, this is something that might change.”
|
||||
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
nothing else in your application. This is what allows for extremely efficient updates
|
||||
to the DOM.
|
||||
|
||||
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary.
|
||||
As a result, you can write a simpler view:
|
||||
As a result, you can write a simpler view:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<button /* ... */>
|
||||
@@ -129,15 +136,17 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
Remember—and this is *very important*—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
Remember—and this is _very important_—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
in a function, telling the framework to update the view every time `count` changes.
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
> show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
> and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# `view`: Dynamic Attributes and Classes
|
||||
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
create dynamic text by passing a function (such as a signal) into the view.
|
||||
|
||||
But of course there are other things you might want to update in your user interface.
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
and we’ll introduce the concept of a **derived signal**.
|
||||
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
increment a counter.
|
||||
|
||||
```rust
|
||||
@@ -34,27 +34,31 @@ So far, this is just the example from the last chapter.
|
||||
|
||||
Now let’s say I’d like to update the list of CSS classes on this element dynamically.
|
||||
For example, let’s say I want to add the class `red` when the count is odd. I can
|
||||
do this using the `class:` syntax.
|
||||
do this using the `class:` syntax.
|
||||
|
||||
```rust
|
||||
class:red=move || count() % 2 == 1
|
||||
```
|
||||
|
||||
`class:` attributes take
|
||||
|
||||
1. the class name, following the colon (`red`)
|
||||
2. a value, which can be a `bool` or a function that returns a `bool`
|
||||
|
||||
When the value is `true`, the class is added. When the value is `false`, the class
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
reactively update when the signal changes.
|
||||
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
the number switches between even and odd.
|
||||
|
||||
## Dynamic Attributes
|
||||
|
||||
The same applies to plain attributes. Passing a plain string or primitive value to
|
||||
an attribute gives it a static value. Passing a function (including a signal) to
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
to our view:
|
||||
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
@@ -63,17 +67,18 @@ to our view:
|
||||
/>
|
||||
```
|
||||
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
our progress bar will move forward.
|
||||
|
||||
## Derived Signals
|
||||
## Derived Signals
|
||||
|
||||
Let’s go one layer deeper, just for fun.
|
||||
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
the `view`. This means that we can easily change our progress bar. For example,
|
||||
suppose we want it to move twice as fast:
|
||||
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
@@ -83,28 +88,31 @@ suppose we want it to move twice as fast:
|
||||
|
||||
But imagine we want to reuse that calculation in more than one place. You can do this
|
||||
using a **derived signal**: a closure that accesses a signal.
|
||||
|
||||
```rust
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
/* insert the rest of the view */
|
||||
/* insert the rest of the view */
|
||||
<progress
|
||||
max="50"
|
||||
// we use it once here
|
||||
value=double_count
|
||||
/>
|
||||
<p>
|
||||
"Double Count: "
|
||||
"Double Count: "
|
||||
// and again here
|
||||
{double_count}
|
||||
</p>
|
||||
```
|
||||
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
places in your application with minimal overhead.
|
||||
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
signal change per place we access `double_count`; in other words, twice. This is a
|
||||
very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
are designed to solve this problem for expensive calculations.
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
> signal change per place we access `double_count`; in other words, twice. This is a
|
||||
> very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
> are designed to solve this problem for expensive calculations.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Components and Props
|
||||
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
interface down into smaller, reusable, composable chunks.
|
||||
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
instead of one: one that advances one tick per click, one that advances two ticks
|
||||
per click.
|
||||
|
||||
@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
value=count
|
||||
@@ -28,7 +28,7 @@ view! {
|
||||
```
|
||||
|
||||
But of course, this doesn’t scale very well. If you want to add a third progress
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
about it, you need to edit it in triplicate.
|
||||
|
||||
Instead, let’s create a `<ProgressBar/>` component.
|
||||
@@ -48,15 +48,15 @@ fn ProgressBar(
|
||||
}
|
||||
```
|
||||
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
Now we need some way to pass an argument into the component.
|
||||
|
||||
## Component Props
|
||||
## Component Props
|
||||
|
||||
We do this using component properties, or “props.” If you’ve used another frontend
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
the component.
|
||||
|
||||
In Leptos, you define props by giving additional arguments to the component function.
|
||||
@@ -70,7 +70,7 @@ fn ProgressBar(
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
// now this works
|
||||
// now this works
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
@@ -93,41 +93,45 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
in as if it were an HTML element attribute. Simple.
|
||||
|
||||
> ### Important Note
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
is what allows us to have named props, when Rust does not have named function parameters.
|
||||
If you’re defining a component in one module and importing it into another, make
|
||||
sure you include this `ComponentProps` type:
|
||||
> ### Important Note
|
||||
>
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
> is what allows us to have named props, when Rust does not have named function parameters.
|
||||
> If you’re defining a component in one module and importing it into another, make
|
||||
> sure you include this `ComponentProps` type:
|
||||
>
|
||||
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||
>
|
||||
> **Note**: This is still true as of `0.2.5`, but the requirement has been removed on `main`
|
||||
> and will not apply to later versions.
|
||||
|
||||
### Reactive and Static Props
|
||||
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
`ReadSignal<i32>`, and not a plain `i32`. This is **very important**.
|
||||
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
be a signal.
|
||||
|
||||
### `optional` Props
|
||||
### `optional` Props
|
||||
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
argument to the component function with `#[prop(optional)]`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
// mark this prop optional
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
max: u16,
|
||||
@@ -143,7 +147,7 @@ fn ProgressBar(
|
||||
```
|
||||
|
||||
Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max`
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
on an `optional` is its `Default::default()` value, which for a `u16` is going to
|
||||
be `0`. In the case of a progress bar, a max value of `0` is not very useful.
|
||||
|
||||
@@ -188,20 +192,20 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
"Click me"
|
||||
</button>
|
||||
<ProgressBar progress=count/>
|
||||
// add a second progress bar
|
||||
// add a second progress bar
|
||||
<ProgressBar progress=double_count/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hm... this won’t compile. It should be pretty easy to understand why: we’ve declared
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
it’s a closure that returns an `i32`.
|
||||
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
|
||||
```rust
|
||||
@@ -211,8 +215,8 @@ fn ProgressBar<F>(
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
) -> impl IntoView
|
||||
where
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> i32 + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
@@ -224,27 +228,26 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
any value that implements this `Fn()` trait.
|
||||
|
||||
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||
or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
> or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
|
||||
### `into` Props
|
||||
### `into` Props
|
||||
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
This attribute automatically calls `.into()` on the values you pass as props,
|
||||
which allows you to easily pass props with different values.
|
||||
|
||||
In this case, it’s helpful to know about the
|
||||
In this case, it’s helpful to know about the
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||
is an enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
is an enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
reactive value.
|
||||
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
@@ -253,7 +256,7 @@ fn ProgressBar(
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
progress: Signal<i32>
|
||||
) -> impl IntoView
|
||||
) -> impl IntoView
|
||||
{
|
||||
view! { cx,
|
||||
<progress
|
||||
@@ -282,12 +285,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
## Documenting Components
|
||||
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
easy, and bears immediate fruit.
|
||||
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
component function, and each one of the props:
|
||||
|
||||
```rust
|
||||
@@ -310,9 +313,11 @@ That’s all you need to do. These behave like ordinary Rust doc comments, excep
|
||||
that you can document individual component props, which can’t be done with Rust
|
||||
function arguments.
|
||||
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
and see the power of the `#[component]` macro combined with rust-analyzer here.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
# Iteration
|
||||
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
iterating over a list of items is a common task in web applications. Reconciling
|
||||
the differences between changing sets of items can also be one of the trickiest
|
||||
tasks for a framework to handle well.
|
||||
|
||||
Leptos supports to two different patterns for iterating over items:
|
||||
|
||||
1. For static views: `Vec<_>`
|
||||
2. For dynamic lists: `<For/>`
|
||||
|
||||
## Static Views with `Vec<_>`
|
||||
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render
|
||||
`T`, you can render `Vec<T>`.
|
||||
|
||||
@@ -58,31 +59,34 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
it changes, this will rerender every item in the list. This is quite inefficient!
|
||||
Fortunately, there’s a better way.
|
||||
|
||||
## Dynamic Rendering with the `<For/>` Component
|
||||
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
keyed dynamic list. It takes three props:
|
||||
|
||||
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
|
||||
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
|
||||
- `view`: renders each `T` into a view
|
||||
- `view`: renders each `T` into a view
|
||||
|
||||
`key` is, well, the key. You can add, remove, and move items within the list. As
|
||||
long as each item’s key is stable over time, the framework does not need to rerender
|
||||
any of the items, unless they are new additions, and it can very efficiently add,
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
to the list as it changes, with minimal additional work.
|
||||
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
indices change.
|
||||
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
it is generated, and using that as an ID for the key function.
|
||||
|
||||
Check out the `<DynamicList/>` component below for an example.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
# Forms and Inputs
|
||||
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
basic patterns for interacting with inputs in Leptos, which you may recognize
|
||||
if you’re familiar with React, SolidJS, or a similar framework: using **controlled**
|
||||
or **uncontrolled** inputs.
|
||||
|
||||
## Controlled Inputs
|
||||
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
state, which in turn updates the `value` prop of the input.
|
||||
|
||||
There are two important things to remember:
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
want `on:input`, but we give you the freedom to choose.
|
||||
2. The `value` *attribute* only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
*property* continues updating the input after that. You usually want to set
|
||||
2. The `value` _attribute_ only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
_property_ continues updating the input after that. You usually want to set
|
||||
`prop:value` for this reason.
|
||||
|
||||
```rust
|
||||
@@ -41,14 +42,14 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
## Uncontrolled Inputs
|
||||
## Uncontrolled Inputs
|
||||
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
the input once when we want to get its value.
|
||||
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
event.
|
||||
|
||||
```rust
|
||||
@@ -56,7 +57,8 @@ let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
|
||||
let input_element: NodeRef<Input> = create_node_ref(cx);
|
||||
```
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
underlying DOM node. Its value will be set when the element is rendered.
|
||||
|
||||
```rust
|
||||
@@ -76,13 +78,14 @@ let on_submit = move |ev: SubmitEvent| {
|
||||
set_name(value);
|
||||
};
|
||||
```
|
||||
|
||||
Our `on_submit` handler will access the input’s value and use it to call `set_name`.
|
||||
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
unwrap here.
|
||||
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
gives us access to a correctly-typed HTML element.
|
||||
|
||||
```rust
|
||||
@@ -97,11 +100,15 @@ view! { cx,
|
||||
<p>"Name is: " {name}</p>
|
||||
}
|
||||
```
|
||||
|
||||
The view should be pretty self-explanatory by now. Note two things:
|
||||
|
||||
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
the browser control its state. (We could use `prop:value` instead.)
|
||||
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
|
||||
They are the same thing, but `node_ref` has better rust-analyzer support.)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -282,4 +282,6 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -110,4 +110,6 @@ Not a number! Errors:
|
||||
If you fix the error, the error message will disappear and the content you’re wrapping in
|
||||
an `<ErrorBoundary/>` will appear again.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -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?
|
||||
@@ -285,4 +285,6 @@ in `<ButtonD/>` and a single text node in `<App/>`. It’s as if the components
|
||||
themselves don’t exist at all. And, well... at runtime, they don’t. It’s just
|
||||
signals and effects, all the way down.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -123,4 +123,6 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
BIN
docs/video/async.mov
Normal file
BIN
docs/video/async.mov
Normal file
Binary file not shown.
BIN
docs/video/in-order.mov
Normal file
BIN
docs/video/in-order.mov
Normal file
Binary file not shown.
BIN
docs/video/out-of-order.mov
Normal file
BIN
docs/video/out-of-order.mov
Normal file
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use counter::*;
|
||||
use counter::SimpleCounter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
[env]
|
||||
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
|
||||
|
||||
[tasks.post-test]
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+stable", "build-all-features"]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
86
examples/counter_without_macros/tests/web.rs
Normal file
86
examples/counter_without_macros/tests/web.rs
Normal 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()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use counters::{Counters, CountersProps};
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
95
examples/router/style.css
Normal 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.125s slideIn forwards;
|
||||
}
|
||||
|
||||
.slideOut {
|
||||
animation: 0.125s 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.125s slideInBack forwards;
|
||||
}
|
||||
|
||||
.slideOutBack {
|
||||
animation: 0.125s 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
54
examples/session_auth_axum/flake.lock
generated
54
examples/session_auth_axum/flake.lock
generated
@@ -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",
|
||||
|
||||
@@ -6,7 +6,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::{post, get},
|
||||
extract::{Path, Extension},
|
||||
extract::{Path, Extension, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
@@ -22,11 +22,12 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_sessions_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, request: Request<AxumBody>) -> impl IntoResponse {
|
||||
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, move |cx| {
|
||||
handle_server_fns_with_context(path, headers, raw_query, move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
provide_context(cx, pool.clone());
|
||||
}, request).await
|
||||
@@ -73,7 +74,7 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(server_fn_handler))
|
||||
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -51,7 +51,8 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
|
||||
@@ -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"]]
|
||||
|
||||
@@ -14,5 +14,6 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
parking_lot = "0.12.1"
|
||||
regex = "1.7.0"
|
||||
|
||||
@@ -18,6 +18,7 @@ use http::StatusCode;
|
||||
use leptos::{
|
||||
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
@@ -150,9 +151,9 @@ pub fn handle_server_fns() -> Route {
|
||||
handle_server_fns_with_context(|_cx| {})
|
||||
}
|
||||
|
||||
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
|
||||
/// Leptos server function arguments in the body, runs the server function if found,
|
||||
/// and returns the resulting [HttpResponse].
|
||||
/// An Actix [Route](actix_web::Route) that listens for `GET` or `POST` requests with
|
||||
/// Leptos server function arguments in the URL (`GET`) or body (`POST`),
|
||||
/// runs the server function if found, and returns the resulting [HttpResponse].
|
||||
///
|
||||
/// This provides the [HttpRequest] to the server [Scope](leptos::Scope).
|
||||
///
|
||||
@@ -168,7 +169,7 @@ pub fn handle_server_fns() -> Route {
|
||||
pub fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> Route {
|
||||
web::post().to(
|
||||
web::to(
|
||||
move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| {
|
||||
let additional_context = additional_context.clone();
|
||||
async move {
|
||||
@@ -194,15 +195,17 @@ pub fn handle_server_fns_with_context(
|
||||
provide_context(cx, req.clone());
|
||||
provide_context(cx, res_options.clone());
|
||||
|
||||
match server_fn(cx, body).await {
|
||||
let query = req.query_string().as_bytes();
|
||||
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -257,9 +260,15 @@ pub fn handle_server_fns_with_context(
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError()
|
||||
.body(e.to_string()),
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().body(
|
||||
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 {:?}. \
|
||||
@@ -289,6 +298,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]
|
||||
@@ -312,6 +322,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// leptos_actix::render_app_to_stream(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
@@ -331,11 +342,12 @@ pub fn handle_server_fns_with_context(
|
||||
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
|
||||
@@ -354,6 +366,7 @@ where
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos_router::Method;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
@@ -377,6 +390,7 @@ where
|
||||
/// leptos_actix::render_app_to_stream_in_order(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
@@ -396,11 +410,17 @@ where
|
||||
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
|
||||
@@ -417,6 +437,7 @@ where
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos_router::Method;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
@@ -440,6 +461,7 @@ where
|
||||
/// leptos_actix::render_app_async(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
@@ -459,11 +481,12 @@ where
|
||||
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
|
||||
@@ -482,11 +505,12 @@ 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();
|
||||
@@ -504,7 +528,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
|
||||
@@ -523,11 +554,12 @@ 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();
|
||||
@@ -546,7 +578,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
|
||||
@@ -566,11 +605,12 @@ 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();
|
||||
@@ -594,7 +634,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
|
||||
@@ -845,7 +892,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,
|
||||
{
|
||||
@@ -854,11 +901,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();
|
||||
|
||||
@@ -868,14 +920,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
|
||||
}
|
||||
@@ -892,7 +949,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
|
||||
@@ -917,7 +974,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
|
||||
@@ -939,7 +996,7 @@ where
|
||||
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
|
||||
@@ -979,7 +1036,7 @@ where
|
||||
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
|
||||
@@ -987,29 +1044,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
|
||||
}
|
||||
|
||||
@@ -16,5 +16,8 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
tokio-util = {version = "0.7.7", features = ["rt"] }
|
||||
once_cell = "1.17"
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::Path,
|
||||
extract::{Path, RawQuery},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
HeaderMap, Request, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
routing::{delete, get, patch, post, put},
|
||||
};
|
||||
use futures::{
|
||||
channel::mpsc::{Receiver, Sender},
|
||||
@@ -24,15 +24,18 @@ use http::{header, method::Method, uri::Uri, version::Version, Response};
|
||||
use hyper::body;
|
||||
use leptos::{
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
ssr::*,
|
||||
*,
|
||||
};
|
||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
||||
use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
||||
use leptos_router::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
use std::{io, pin::Pin, sync::Arc};
|
||||
use tokio::task::{spawn_blocking, LocalSet};
|
||||
use std::{io, pin::Pin, sync::Arc, thread::available_parallelism};
|
||||
use tokio::task::LocalSet;
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
|
||||
/// 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
|
||||
@@ -248,9 +251,10 @@ where
|
||||
pub async fn handle_server_fns(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
RawQuery(query): RawQuery,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
handle_server_fns_inner(fn_name, headers, |_| {}, req).await
|
||||
handle_server_fns_inner(fn_name, headers, query, |_| {}, req).await
|
||||
}
|
||||
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
@@ -270,15 +274,18 @@ pub async fn handle_server_fns(
|
||||
pub async fn handle_server_fns_with_context(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap,
|
||||
RawQuery(query): RawQuery,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
handle_server_fns_inner(fn_name, headers, additional_context, req).await
|
||||
handle_server_fns_inner(fn_name, headers, query, additional_context, req)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_server_fns_inner(
|
||||
fn_name: String,
|
||||
headers: HeaderMap,
|
||||
query: Option<String>,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -289,133 +296,116 @@ async fn handle_server_fns_inner(
|
||||
.unwrap_or(fn_name);
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
spawn_blocking({
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
async move {
|
||||
let res = if let Some(server_fn) =
|
||||
server_fn_by_path(fn_name.as_str())
|
||||
let pool_handle = get_leptos_pool();
|
||||
pool_handle.spawn_pinned(move || {
|
||||
async move {
|
||||
let res = if let Some(server_fn) =
|
||||
server_fn_by_path(fn_name.as_str())
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
|
||||
additional_context(cx);
|
||||
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await; // Add this so we can get details about the Request
|
||||
provide_context(cx, req_parts.clone());
|
||||
provide_context(cx, leptos_req);
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
let query: &Bytes = &query.unwrap_or("".to_string()).into();
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => &req_parts.body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => query,
|
||||
};
|
||||
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);
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = headers
|
||||
.get("Accept")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
// Add headers from ResponseParts if they exist. These should be added as long
|
||||
// as the server function returns an OK response
|
||||
let res_options_outer = res_options.unwrap().0;
|
||||
let res_options_inner = res_options_outer.read();
|
||||
let (status, mut res_headers) = (
|
||||
res_options_inner.status,
|
||||
res_options_inner.headers.clone(),
|
||||
);
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) =
|
||||
raw_scope_and_disposer(runtime);
|
||||
|
||||
additional_context(cx);
|
||||
|
||||
let (req, req_parts) =
|
||||
generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await; // Add this so we can get details about the Request
|
||||
provide_context(cx, req_parts.clone());
|
||||
provide_context(cx, leptos_req);
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
match server_fn(cx, &req_parts.body).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx);
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header = headers
|
||||
.get("Accept")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
// Add headers from ResponseParts if they exist. These should be added as long
|
||||
// as the server function returns an OK response
|
||||
let res_options_outer =
|
||||
res_options.unwrap().0;
|
||||
let res_options_inner =
|
||||
res_options_outer.read();
|
||||
let (status, mut res_headers) = (
|
||||
res_options_inner.status,
|
||||
res_options_inner.headers.clone(),
|
||||
);
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some(
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
|| accept_header
|
||||
== Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = headers
|
||||
.get("Referer")
|
||||
.and_then(|value| {
|
||||
value.to_str().ok()
|
||||
})
|
||||
.unwrap_or("/");
|
||||
|
||||
res = res
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header("Location", referer);
|
||||
}
|
||||
// Override StatusCode if it was set in a Resource or Element
|
||||
res = match status {
|
||||
Some(status) => res.status(status),
|
||||
None => res,
|
||||
};
|
||||
// This must be after the default referrer
|
||||
// redirect so that it overwrites the one above
|
||||
if let Some(header_ref) = res.headers_mut()
|
||||
{
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/cbor",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Json(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/json",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
}
|
||||
}
|
||||
Err(e) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Full::from(e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Full::from(format!(
|
||||
"Could not find a server function at the \
|
||||
route {fn_name}. \n\nIt's likely that \
|
||||
you need to call ServerFn::register() on \
|
||||
the server function type, somewhere in \
|
||||
your `main` function."
|
||||
)))
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
.expect("could not build Response");
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = headers
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
|
||||
_ = tx.send(res);
|
||||
res = res
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header("Location", referer);
|
||||
}
|
||||
// Override StatusCode if it was set in a Resource or Element
|
||||
res = match status {
|
||||
Some(status) => res.status(status),
|
||||
None => res,
|
||||
};
|
||||
// This must be after the default referrer
|
||||
// redirect so that it overwrites the one above
|
||||
if let Some(header_ref) = res.headers_mut() {
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header("Content-Type", "application/cbor")
|
||||
.body(Full::from(data)),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Json(data) => res
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Full::from(data)),
|
||||
}
|
||||
}
|
||||
})
|
||||
Err(e) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Full::from(
|
||||
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!(
|
||||
"Could not find a server function at the route \
|
||||
{fn_name}. \n\nIt's likely that you need to call \
|
||||
ServerFn::register() on the server function type, \
|
||||
somewhere in your `main` function."
|
||||
)),
|
||||
)
|
||||
}
|
||||
.expect("could not build Response");
|
||||
|
||||
_ = tx.send(res);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -619,56 +609,33 @@ where
|
||||
let default_res_options = ResponseOptions::default();
|
||||
let res_options2 = default_res_options.clone();
|
||||
let res_options3 = default_res_options.clone();
|
||||
let local_pool = get_leptos_pool();
|
||||
let (tx, rx) = futures::channel::mpsc::channel(8);
|
||||
local_pool.spawn_pinned(move || async move {
|
||||
let app = {
|
||||
// Need to get the path and query string of the Request
|
||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
|
||||
let path = req.uri().path_and_query().unwrap().as_str();
|
||||
|
||||
async move {
|
||||
// Need to get the path and query string of the Request
|
||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
|
||||
let path = req.uri().path_and_query().unwrap().as_str();
|
||||
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::channel(8);
|
||||
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
});
|
||||
};
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
generate_response(res_options3, rx).await
|
||||
}
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
});
|
||||
async move { generate_response(res_options3, rx).await }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -798,42 +765,26 @@ where
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::channel(8);
|
||||
let local_pool = get_leptos_pool();
|
||||
local_pool.spawn_pinned(move || async move {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
let (bundle, runtime, scope) =
|
||||
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|cx| generate_head_metadata_separated(cx).1.into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
|
||||
});
|
||||
|
||||
generate_response(res_options3, rx).await
|
||||
@@ -981,53 +932,39 @@ where
|
||||
let full_path = format!("http://leptos.dev{path}");
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
let local_pool = get_leptos_pool();
|
||||
local_pool.spawn_pinned(move || {
|
||||
async move {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
let add_context = add_context.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let app = {
|
||||
let full_path = full_path.clone();
|
||||
let (req, req_parts) = generate_request_and_parts(req).await;
|
||||
let leptos_req = generate_leptos_request(req).await;
|
||||
move |cx| {
|
||||
provide_contexts(cx, full_path, req_parts,leptos_req, default_res_options);
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|_| "".into(),
|
||||
add_context,
|
||||
);
|
||||
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
|_| "".into(),
|
||||
add_context,
|
||||
);
|
||||
// Extract the value of ResponseOptions from here
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
// Extract the value of ResponseOptions from here
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
let html = build_async_response(stream, &options, runtime, scope).await;
|
||||
|
||||
let html = build_async_response(stream, &options, runtime, scope).await;
|
||||
let new_res_parts = res_options.0.read().clone();
|
||||
|
||||
let new_res_parts = res_options.0.read().clone();
|
||||
let mut writable = res_options2.0.write();
|
||||
*writable = new_res_parts;
|
||||
|
||||
let mut writable = res_options2.0.write();
|
||||
*writable = new_res_parts;
|
||||
|
||||
_ = tx.send(html);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
_ = tx.send(html);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1054,12 +991,12 @@ where
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum 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();
|
||||
@@ -1083,17 +1020,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
|
||||
}
|
||||
@@ -1105,7 +1051,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
|
||||
@@ -1114,7 +1060,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
|
||||
@@ -1123,7 +1069,7 @@ pub trait LeptosRoutes {
|
||||
|
||||
fn leptos_routes_with_handler<H, T>(
|
||||
self,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1136,7 +1082,7 @@ impl LeptosRoutes for axum::Router {
|
||||
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
|
||||
@@ -1148,7 +1094,7 @@ impl LeptosRoutes for axum::Router {
|
||||
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
|
||||
@@ -1156,38 +1102,65 @@ 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
|
||||
}
|
||||
|
||||
fn leptos_routes_with_handler<H, T>(
|
||||
self,
|
||||
paths: Vec<(String, SsrMode)>,
|
||||
paths: Vec<RouteListing>,
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
@@ -1195,9 +1168,33 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
fn get_leptos_pool() -> LocalPoolHandle {
|
||||
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();
|
||||
LOCAL_POOL
|
||||
.get_or_init(|| {
|
||||
tokio_util::task::LocalPoolHandle::new(
|
||||
available_parallelism().map(Into::into).unwrap_or(1),
|
||||
)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
@@ -14,6 +14,7 @@ use http::{header, method::Method, uri::Uri, version::Version, StatusCode};
|
||||
use hyper::body;
|
||||
use leptos::{
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
ssr::*,
|
||||
*,
|
||||
};
|
||||
@@ -185,6 +186,7 @@ async fn handle_server_fns_inner(
|
||||
) -> Result<Response> {
|
||||
let fn_name = req.params::<String>()?;
|
||||
let headers = req.headers().clone();
|
||||
let query = req.query_string().unwrap_or("").to_owned().into();
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
spawn_blocking({
|
||||
move || {
|
||||
@@ -207,16 +209,21 @@ async fn handle_server_fns_inner(
|
||||
// Add this so that we can set headers and status of the response
|
||||
provide_context(cx, ResponseOptions::default());
|
||||
|
||||
match server_fn(cx, &req_parts.body).await {
|
||||
let data = match &server_fn.encoding {
|
||||
Encoding::Url | Encoding::Cbor => {
|
||||
&req_parts.body
|
||||
}
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
|
||||
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")
|
||||
@@ -292,8 +299,15 @@ async fn handle_server_fns_inner(
|
||||
}
|
||||
Err(e) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::from(e.to_string())),
|
||||
}
|
||||
.body(Body::from(
|
||||
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)
|
||||
@@ -932,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();
|
||||
@@ -961,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
|
||||
}
|
||||
@@ -983,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
|
||||
@@ -992,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
|
||||
@@ -1001,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
|
||||
@@ -1014,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
|
||||
@@ -1026,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())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -180,7 +182,9 @@ pub use for_loop::*;
|
||||
pub use show::*;
|
||||
mod suspense;
|
||||
pub use suspense::*;
|
||||
mod text_prop;
|
||||
mod transition;
|
||||
pub use text_prop::TextProp;
|
||||
#[cfg(debug_assertions)]
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
@@ -218,3 +222,42 @@ pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
|
||||
/// }
|
||||
/// ```
|
||||
pub type AttributeValue = Box<dyn IntoAttribute>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Component<P> {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait Props {
|
||||
type Builder;
|
||||
fn builder() -> Self::Builder;
|
||||
}
|
||||
|
||||
impl<P, F, R> Component<P> for F where F: FnOnce(::leptos::Scope, P) -> R {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_props_builder<P: Props>(
|
||||
_f: &impl Component<P>,
|
||||
) -> <P as Props>::Builder {
|
||||
<P as Props>::builder()
|
||||
}
|
||||
|
||||
#[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."
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use leptos::component;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos_reactive::{create_memo, signal_prelude::*, Scope};
|
||||
use leptos_reactive::{create_memo, signal_prelude::*, Scope, ScopeDisposer};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
/// A component that will show its children when the `when` condition is `true`,
|
||||
/// and show the fallback when it is `false`, without rerendering every time
|
||||
@@ -45,9 +46,18 @@ where
|
||||
IV: IntoView,
|
||||
{
|
||||
let memoized_when = create_memo(cx, move |_| when());
|
||||
let prev_disposer = Rc::new(RefCell::new(None::<ScopeDisposer>));
|
||||
|
||||
move || match memoized_when.get() {
|
||||
true => children(cx).into_view(cx),
|
||||
false => fallback(cx).into_view(cx),
|
||||
move || {
|
||||
if let Some(disposer) = prev_disposer.take() {
|
||||
disposer.dispose();
|
||||
}
|
||||
let (view, disposer) =
|
||||
cx.run_child_scope(|cx| match memoized_when.get() {
|
||||
true => children(cx).into_view(cx),
|
||||
false => fallback(cx).into_view(cx),
|
||||
});
|
||||
*prev_disposer.borrow_mut() = Some(disposer);
|
||||
view
|
||||
}
|
||||
}
|
||||
|
||||
43
leptos/src/text_prop.rs
Normal file
43
leptos/src/text_prop.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ features = [
|
||||
"TreeWalker",
|
||||
|
||||
# Events we cast to in leptos_macro -- added here so we don't force users to import them
|
||||
"AddEventListenerOptions",
|
||||
"AnimationEvent",
|
||||
"BeforeUnloadEvent",
|
||||
"ClipboardEvent",
|
||||
|
||||
@@ -135,6 +135,7 @@ impl Mountable for ComponentRepr {
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -159,8 +161,10 @@ where
|
||||
debug_assertions,
|
||||
instrument(level = "trace", 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)] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -43,17 +43,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 +66,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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,16 +22,23 @@ 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(),
|
||||
event_name,
|
||||
event_handler,
|
||||
&None,
|
||||
);
|
||||
} else {
|
||||
add_event_listener_undelegated(target, &event_name, event_handler);
|
||||
add_event_listener_undelegated(
|
||||
target,
|
||||
&event_name,
|
||||
event_handler,
|
||||
&None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,27 +49,28 @@ 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,
|
||||
{
|
||||
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);
|
||||
add_delegated_event_listener(&key, event_name, options);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -69,26 +78,37 @@ 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();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
let cb = Closure::wrap(cb as Box<dyn FnMut(E)>).into_js_value();
|
||||
if let Some(options) = options {
|
||||
_ = target
|
||||
.add_event_listener_with_callback_and_add_event_listener_options(
|
||||
event_name,
|
||||
cb.unchecked_ref(),
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
_ = target
|
||||
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
}
|
||||
|
||||
// cf eventHandler in ryansolid/dom-expressions
|
||||
@@ -96,6 +116,7 @@ pub(crate) fn add_event_listener_undelegated<E>(
|
||||
pub(crate) fn add_delegated_event_listener(
|
||||
key: &str,
|
||||
event_name: Cow<'static, str>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) {
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
let mut events = global_events.borrow_mut();
|
||||
@@ -167,10 +188,19 @@ pub(crate) fn add_delegated_event_listener(
|
||||
|
||||
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
|
||||
let handler = Closure::wrap(handler).into_js_value();
|
||||
_ = crate::window().add_event_listener_with_callback(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
);
|
||||
if let Some(options) = options {
|
||||
_ = crate::window().add_event_listener_with_callback_and_add_event_listener_options(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
_ = crate::window().add_event_listener_with_callback(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// register that we've created handler
|
||||
events.insert(event_name);
|
||||
|
||||
@@ -8,19 +8,24 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,22 +38,23 @@ 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.
|
||||
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
|
||||
name: Cow<'static, str>,
|
||||
options: Option<web_sys::AddEventListenerOptions>,
|
||||
_event_type: PhantomData<E>,
|
||||
}
|
||||
|
||||
@@ -56,6 +62,7 @@ impl<E: FromWasmAbi> Clone for Custom<E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
options: self.options.clone(),
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -72,8 +79,11 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +94,35 @@ impl<E: FromWasmAbi> Custom<E> {
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
options: None,
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the [`AddEventListenerOptions`] used for this event listener.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # let canvas_ref: NodeRef<html::Canvas> = create_node_ref(cx);
|
||||
/// let mut non_passive_wheel = ev::Custom::<ev::WheelEvent>::new("wheel");
|
||||
/// # if false {
|
||||
/// let options = non_passive_wheel.options_mut();
|
||||
/// options.passive(false);
|
||||
/// # }
|
||||
/// canvas_ref.on_load(cx, move |canvas: HtmlElement<html::Canvas>| {
|
||||
/// canvas.on(non_passive_wheel, move |_event| {
|
||||
/// // Handle _event
|
||||
/// });
|
||||
/// });
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// [`AddEventListenerOptions`]: web_sys::AddEventListenerOptions
|
||||
pub fn options_mut(&mut self) -> &mut web_sys::AddEventListenerOptions {
|
||||
self.options
|
||||
.get_or_insert_with(web_sys::AddEventListenerOptions::new)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! generate_event_types {
|
||||
@@ -104,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! {
|
||||
|
||||
@@ -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.
|
||||
@@ -195,6 +209,7 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
debug_assertions,
|
||||
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,
|
||||
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},
|
||||
@@ -347,6 +367,7 @@ pub fn set_interval(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn set_interval_with_handle(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
@@ -363,13 +384,22 @@ 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`.
|
||||
@@ -377,6 +407,7 @@ pub fn set_interval_with_handle(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(event_name = %event_name))
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn window_event_listener(
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
@@ -394,11 +425,16 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(),
|
||||
@@ -685,10 +702,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 +710,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 +841,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,19 +864,22 @@ 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,
|
||||
event_name,
|
||||
event_handler,
|
||||
event.options(),
|
||||
);
|
||||
} else {
|
||||
add_event_listener_undelegated(
|
||||
self.element.as_ref(),
|
||||
&event_name,
|
||||
event_handler,
|
||||
event.options(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -920,6 +945,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();
|
||||
|
||||
@@ -943,6 +969,7 @@ 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(all(target_arch = "wasm32", feature = "web"), inline(always))]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
@@ -1009,6 +1036,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())
|
||||
}
|
||||
@@ -1070,6 +1098,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"))]
|
||||
{
|
||||
@@ -1083,6 +1112,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;
|
||||
@@ -1093,11 +1123,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
|
||||
}
|
||||
@@ -1125,6 +1157,7 @@ macro_rules! generate_html_tags {
|
||||
};
|
||||
(@void) => {};
|
||||
(@void void) => {
|
||||
#[inline(always)]
|
||||
fn is_void(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,13 +687,14 @@ impl View {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
match &self {
|
||||
Self::Element(el) => {
|
||||
if event.bubbles() {
|
||||
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler);
|
||||
if E::BUBBLES {
|
||||
add_event_listener(&el.element, event.event_delegation_key(), event.name(), event_handler, &None);
|
||||
} else {
|
||||
add_event_listener_undelegated(
|
||||
&el.element,
|
||||
&event.name(),
|
||||
event_handler,
|
||||
&None,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -918,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;
|
||||
@@ -992,12 +1001,14 @@ impl IntoView for String {
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "#text", skip_all)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Text(Text::new(self.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Text(Text::new(self.into()))
|
||||
}
|
||||
@@ -1019,6 +1030,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()))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
@@ -91,7 +95,7 @@ pub(crate) fn property_helper(
|
||||
match value {
|
||||
Property::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
create_render_effect(cx, move |_| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(&el, prop_name, new.clone());
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -244,17 +244,28 @@ fn fragments_to_chunks(
|
||||
impl View {
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
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> {
|
||||
pub(crate) fn render_to_string_helper(
|
||||
self,
|
||||
dont_escape_text: bool,
|
||||
) -> Cow<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => node.content,
|
||||
View::Text(node) => {
|
||||
if dont_escape_text {
|
||||
node.content
|
||||
} else {
|
||||
html_escape::encode_safe(&node.content).to_string().into()
|
||||
}
|
||||
}
|
||||
View::Component(node) => {
|
||||
let content = || {
|
||||
node.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.map(|node| {
|
||||
node.render_to_string_helper(dont_escape_text)
|
||||
})
|
||||
.join("")
|
||||
};
|
||||
cfg_if! {
|
||||
@@ -281,7 +292,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) => {
|
||||
@@ -331,7 +343,9 @@ impl View {
|
||||
t.content
|
||||
}
|
||||
} else {
|
||||
child.render_to_string_helper()
|
||||
child.render_to_string_helper(
|
||||
dont_escape_text,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
@@ -354,7 +368,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)]
|
||||
@@ -407,6 +423,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
|
||||
{
|
||||
@@ -414,9 +432,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()
|
||||
@@ -458,7 +475,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,
|
||||
|
||||
@@ -213,7 +213,7 @@ impl View {
|
||||
/// Renders the view into a set of HTML chunks that can be streamed.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ impl View {
|
||||
self,
|
||||
cx: Scope,
|
||||
chunks: &mut VecDeque<StreamChunk>,
|
||||
dont_escape_text: bool,
|
||||
) {
|
||||
match self {
|
||||
View::Suspense(id, _) => {
|
||||
@@ -241,18 +242,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 +270,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 +326,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 +399,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!(
|
||||
"<!>{}",
|
||||
t.content
|
||||
content
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(t.content)
|
||||
StreamChunk::Sync(content)
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
cx,
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -435,7 +458,9 @@ impl View {
|
||||
);
|
||||
node.child
|
||||
.into_stream_chunks_helper(
|
||||
cx, chunks,
|
||||
cx,
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
@@ -447,6 +472,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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user