mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
166 Commits
optional-p
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059c1bf61c | ||
|
|
add13fd6a4 | ||
|
|
904c2e8a67 | ||
|
|
a5c3be586a | ||
|
|
9f5139d929 | ||
|
|
bae305340e | ||
|
|
40c1556f29 | ||
|
|
0db4f5821f | ||
|
|
12ebc95800 | ||
|
|
d7b919032e | ||
|
|
be8bf8b0d6 | ||
|
|
f84f1422f4 | ||
|
|
b01976e3bb | ||
|
|
50b48fb272 | ||
|
|
1617e31d69 | ||
|
|
51cd082d4c | ||
|
|
72414b7945 | ||
|
|
1afa14ccbd | ||
|
|
477c29cdf1 | ||
|
|
49a424314a | ||
|
|
598523cd9d | ||
|
|
1fdb6f1cdf | ||
|
|
9997487a9c | ||
|
|
b5e94e4054 | ||
|
|
a5f6e0bac4 | ||
|
|
2c9de79576 | ||
|
|
63dd00a050 | ||
|
|
99823a3d4f | ||
|
|
c0bdd464f6 | ||
|
|
7e7377f4f7 | ||
|
|
15448765dd | ||
|
|
f0f1c3144b | ||
|
|
630da4212d | ||
|
|
38bc24bb9e | ||
|
|
012285337b | ||
|
|
3ba4f62cef | ||
|
|
b4996769c1 | ||
|
|
9a6b1f53da | ||
|
|
ef45828ca7 | ||
|
|
ea153e4f26 | ||
|
|
59b8626277 | ||
|
|
d8e03773f0 | ||
|
|
5ab799bbf8 | ||
|
|
6c763a83cb | ||
|
|
9cf337309d | ||
|
|
1af35cdd3b | ||
|
|
fcb98474b8 | ||
|
|
54f7e9366a | ||
|
|
ddf9df2b5e | ||
|
|
7fe9f82d89 | ||
|
|
661adc4027 | ||
|
|
1011c464dc | ||
|
|
4b498a3b42 | ||
|
|
3c90b47e77 | ||
|
|
671b1e4a8f | ||
|
|
52021be806 | ||
|
|
75a7bd610a | ||
|
|
de553cf4fe | ||
|
|
0a65f43789 | ||
|
|
0f277c55ec | ||
|
|
04b01a6ced | ||
|
|
6c3381ce52 | ||
|
|
fa2e2248d3 | ||
|
|
362150a715 | ||
|
|
27b5991ee3 | ||
|
|
0a7dbb0ca4 | ||
|
|
234861a156 | ||
|
|
78d6d312f8 | ||
|
|
a1144a5b6b | ||
|
|
9723cc466e | ||
|
|
79c12c0129 | ||
|
|
a08d6bae10 | ||
|
|
39261a276c | ||
|
|
c471986024 | ||
|
|
d2e3a156e8 | ||
|
|
9badfa997b | ||
|
|
72f8bf4e20 | ||
|
|
c74b15b120 | ||
|
|
9a4f3ab08c | ||
|
|
a0935c169e | ||
|
|
0e2181fb90 | ||
|
|
732ec14302 | ||
|
|
ec95060b6e | ||
|
|
689afec26e | ||
|
|
bbf23ea40a | ||
|
|
34e0a8e47d | ||
|
|
81f330e888 | ||
|
|
e5d657dd55 | ||
|
|
f919127a7e | ||
|
|
2001bd808f | ||
|
|
f51857cedc | ||
|
|
f3b8d27c4f | ||
|
|
d3a577c365 | ||
|
|
b80f9e3871 | ||
|
|
328d42656d | ||
|
|
d3d2cbed7e | ||
|
|
d6f7aedec1 | ||
|
|
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 | ||
|
|
60b96c9118 | ||
|
|
7ccb2d9f44 | ||
|
|
2c2090a194 | ||
|
|
de9b2998ac | ||
|
|
29b81a3d50 | ||
|
|
5bc0d89ce7 | ||
|
|
342b10c232 | ||
|
|
ba9d3c1602 | ||
|
|
d3b3ce6980 | ||
|
|
4b79a91287 | ||
|
|
de06c9b2ca | ||
|
|
84c7d00ea9 | ||
|
|
8f5ae0054d | ||
|
|
374f0c4e27 | ||
|
|
a6170f4da9 | ||
|
|
578dd5ef35 | ||
|
|
934a131deb | ||
|
|
5bc1c36e67 | ||
|
|
b1b9853f92 | ||
|
|
5d6a083d1d | ||
|
|
b51da35a9a | ||
|
|
164dcd1b97 | ||
|
|
c0964c2b01 | ||
|
|
af5b226e53 | ||
|
|
3a1db3a191 | ||
|
|
54370e3153 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.5"
|
||||
version = "0.3.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.5" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.5" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.5" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.5" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.5" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
|
||||
leptos_router = { path = "./router", version = "0.2.5" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.2.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0" }
|
||||
leptos_router = { path = "./router", version = "0.3.0" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -49,6 +49,7 @@ dependencies = [
|
||||
{ name = "check", path = "examples/parent_child" },
|
||||
{ name = "check", path = "examples/router" },
|
||||
{ name = "check", path = "examples/session_auth_axum" },
|
||||
{ name = "check", path = "examples/slots" },
|
||||
{ name = "check", path = "examples/ssr_modes" },
|
||||
{ name = "check", path = "examples/ssr_modes_axum" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
@@ -68,15 +69,52 @@ dependencies = [
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
dependencies = [
|
||||
"test-all",
|
||||
"test-leptos_macro-example",
|
||||
"doc-leptos_macro-example",
|
||||
]
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test", "--doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "doc"]
|
||||
cwd = "leptos_macro/example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.test-examples]
|
||||
description = "Run all unit and web tests for examples"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "test-unit-and-web"]
|
||||
|
||||
[tasks.verify-examples]
|
||||
description = "Run all quality checks and tests for examples"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "verify-flow"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
description = "Clean all example projects"
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "clean-all"]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS=""
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS="-D warnings"
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
13
README.md
13
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
|
||||
|
||||
@@ -65,9 +66,9 @@ Here are some resources for learning more about Leptos:
|
||||
|
||||
## `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
|
||||
@@ -75,6 +76,14 @@ rustup default nightly
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
|
||||
|
||||
```toml
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
```
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
|
||||
|
||||
@@ -10,8 +10,8 @@ fn leptos_deep_creation(b: &mut Bencher) {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
for _ in 0..1000usize {
|
||||
let prev = memos.last().copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
@@ -34,9 +34,8 @@ fn leptos_deep_update(b: &mut Bencher) {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
@@ -242,9 +241,8 @@ fn l021_deep_creation(b: &mut Bencher) {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
@@ -266,9 +264,8 @@ fn l021_deep_update(b: &mut Bencher) {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
@@ -444,9 +441,8 @@ fn sycamore_deep_creation(b: &mut Bencher) {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
@@ -465,9 +461,8 @@ fn sycamore_deep_update(b: &mut Bencher) {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
|
||||
@@ -28,6 +28,52 @@ let (a, set_a) = create_signal(cx, 0);
|
||||
let b = move || a () > 5;
|
||||
```
|
||||
|
||||
### Nested signal updates/reads triggering panic
|
||||
|
||||
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let resources = create_rw_signal(cx, HashMap::new());
|
||||
|
||||
let update = move |id: usize| {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(cx, 0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
|
||||
<button on:click=move |_| update(1)>"+"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clicking the button twice will cause a panic, because of the nested signal *read*. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
|
||||
|
||||
You can fix this fairly easily by using the [`Scope::batch()`](https://docs.rs/leptos/latest/leptos/struct.Scope.html#method.batch) method:
|
||||
|
||||
```rust
|
||||
let update = move |id: usize| {
|
||||
cx.batch(move || {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(cx, 0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
|
||||
|
||||
## Templates and the DOM
|
||||
|
||||
### `<input value=...>` doesn't update or stops updating
|
||||
|
||||
@@ -171,4 +171,4 @@ data flow and of fine-grained reactive updates.
|
||||
|
||||
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh">
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- [Suspense](./async/11_suspense.md)
|
||||
- [Transition](./async/12_transition.md)
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Interlude: Projecting Children](./interlude_projecting_children.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router](./router/README.md)
|
||||
@@ -27,22 +28,25 @@
|
||||
- [Params and Queries](./router/18_params_and_queries.md)
|
||||
- [`<A/>`](./router/19_a.md)
|
||||
- [`<Form/>`](./router/20_form.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Interlude: Styling](./interlude_styling.md)
|
||||
- [Metadata]()
|
||||
- [SSR]()
|
||||
- [Models of SSR]()
|
||||
- [`cargo-leptos`]()
|
||||
- [Hydration Footguns]()
|
||||
- [Request/Response]()
|
||||
- [Server Side Rendering](./ssr/README.md)
|
||||
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
|
||||
- [The Life of a Page Load](./ssr/22_life_cycle.md)
|
||||
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
|
||||
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
|
||||
- [Working with the Server](./server/README.md)
|
||||
- [Server Functions](./server/25_server_functions.md)
|
||||
- [Request/Response]()
|
||||
- [Extractors]()
|
||||
- [Axum]()
|
||||
- [Actix]()
|
||||
- [Headers]()
|
||||
- [Cookies]()
|
||||
- [Server Functions]()
|
||||
- [Building Full-Stack Apps]()
|
||||
- [Actions]()
|
||||
- [Forms]()
|
||||
- [`<ActionForm/>`s]()
|
||||
- [Turning off WebAssembly]()
|
||||
- [Turning off WebAssembly: Progressive Enhancement and Graceful Degradation]()
|
||||
- [Advanced Reactivity]()
|
||||
- [Appendix: Optimizing WASM Binary Size]()
|
||||
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)
|
||||
|
||||
58
docs/book/src/appendix_binary_size.md
Normal file
58
docs/book/src/appendix_binary_size.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Appendix: Optimizing WASM Binary Size
|
||||
|
||||
One of the primary downsides of deploying a Rust/WebAssembly frontend app is that splitting a WASM file into smaller chunks to be dynamically loaded is significantly more difficult than splitting a JavaScript bundle. There have been experiments like [`wasm-split`](https://emscripten.org/docs/optimizing/Module-Splitting.html) in the Emscripten ecosystem but at present there’s no way to split and dynamically load a Rust/`wasm-bindgen` binary. This means that the whole WASM binary needs to be loaded before your app becomes interactive. Because the WASM format is designed for streaming compilation, WASM files are much faster to compile per kilobyte than JavaScript files. (For a deeper look, you can [read this great article from the Mozilla team](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/) on streaming WASM compilation.)
|
||||
|
||||
Still, it’s important to ship the smallest WASM binary to users that you can, as it will reduce their network usage and make your app interactive as quickly as possible.
|
||||
|
||||
So what are some practical steps?
|
||||
|
||||
## Things to Do
|
||||
|
||||
1. Make sure you’re looking at a release build. (Debug builds are much, much larger.)
|
||||
2. Add a release profile for WASM that optimizes for size, not speed.
|
||||
|
||||
For a `cargo-leptos` project, for example, you can add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
# ....
|
||||
|
||||
[package.metadata.leptos]
|
||||
# ....
|
||||
lib-profile-release = "wasm-release"
|
||||
```
|
||||
|
||||
This will hyper-optimize the WASM for your release build for size, while keeping your server build optimized for speed. (For a pure client-rendered app without server considerations, just use the `[profile.wasm-release]` block as your `[profile.release]`.)
|
||||
|
||||
3. Always serve compressed WASM in production. WASM tends to compress very well, typically shrinking to less than 50% its uncompressed size, and it’s trivial to enable compression for static files being served from Actix or Axum.
|
||||
|
||||
4. If you’re using nightly Rust, you can rebuild the standard library with this same profile rather than the prebuilt standard library that’s distributed with the `wasm32-unknown-unknown` target.
|
||||
|
||||
To do this, create a file in your project at `.cargo/config.toml`
|
||||
|
||||
```toml
|
||||
[unstable]
|
||||
build-std = ["std", "panic_abort", "core", "alloc"]
|
||||
build-std-features = ["panic_immediate_abort"]
|
||||
```
|
||||
|
||||
5. One of the sources of binary size in WASM binaries can be `serde` serialization/deserialization code. Leptos uses `serde` by default to serialize and deserialize resources created with `create_resource`. You might try experimenting with the `miniserde` and `serde-lite` features, which allow you to use those crates for serialization and deserialization instead; each only implements a subset of `serde`’s functionality, but typically optimizes for size over speed.
|
||||
|
||||
## Things to Avoid
|
||||
|
||||
There are certain crates that tend to inflate binary sizes. For example, the `regex` crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!) In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what `leptos_router` does on the few occasions it needs a regular expression.)
|
||||
|
||||
In general, Rust’s commitment to runtime performance is sometimes at odds with a commitment to a small binary. For example, Rust monomorphizes generic functions, meaning it creates a distinct copy of the function for each generic type it’s called with. This is significantly faster than dynamic dispatch, but increases binary size. Leptos tries to balance runtime performance with binary size considerations pretty carefully; but you might find that writing code that uses many generics tends to increase binary size. For example, if you have a generic component with a lot of code in its body and call it with four different types, remember that the compiler could include four copies of that same code. Refactoring to use a concrete inner function or helper can often maintain performance and ergonomics while reducing binary size.
|
||||
|
||||
## A Final Thought
|
||||
|
||||
Remember that in a server-rendered app, JS bundle size/WASM binary size affects only _one_ thing: time to interactivity on the first load. This is very important to a good user experience—nobody wants to click a button three times and have it do nothing because the interactive code is still loading—but it is not the only important measure.
|
||||
|
||||
It’s especially worth remembering that streaming in a single WASM binary means all subsequent navigations are nearly instantaneous, depending only on any additional data loading. Precisely because your WASM binary is _not_ bundle split, navigating to a new route does not require loading additional JS/WASM, as it does in nearly every JavaScript framework. Is this copium? Maybe. Or maybe it’s just an honest trade-off between the two approaches!
|
||||
|
||||
Always take the opportunity to optimize the low-hanging fruit in your application. And always test your app under real circumstances with real user network speeds and devices before making any heroic efforts.
|
||||
@@ -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)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Working with `async`
|
||||
|
||||
So far we’ve only been working with synchronous users interfaces: You provide some input,
|
||||
the app immediately process it and updates the interface. This is great, but is a tiny
|
||||
subset of what web applications do. In particular, most web apps have to deal with some kind
|
||||
of asynchronous data loading, usually loading something from an API.
|
||||
the app immediately processes it and updates the interface. This is great, but is a tiny
|
||||
subset of what web applications do. In particular, most web apps have to deal with some kind of asynchronous data loading, usually loading something from an API.
|
||||
|
||||
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code. Leptos provides a cross-platform [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html) function that makes it easy to run a `Future`, but there’s much more to it than that.
|
||||
|
||||
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
|
||||
In this chapter, we’ll see how Leptos helps smooth out that process for you.
|
||||
|
||||
177
docs/book/src/interlude_projecting_children.md
Normal file
177
docs/book/src/interlude_projecting_children.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Projecting Children
|
||||
|
||||
As you build components you may occasionally find yourself wanting to “project” children through multiple layers of components.
|
||||
|
||||
## The Problem
|
||||
|
||||
Consider the following:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
<Show
|
||||
// check whether user is verified
|
||||
// by reading from the resource
|
||||
when=move || todo!()
|
||||
fallback=fallback
|
||||
>
|
||||
{children(cx)}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is pretty straightforward: when the user is logged in, we want to show `children`. Until if the user is not logged in, we want to show `fallback`. And while we’re waiting to find out, we just render `()`, i.e., nothing.
|
||||
|
||||
In other words, we want to pass the children of `<WhenLoaded/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
|
||||
|
||||
This won’t compile.
|
||||
|
||||
```
|
||||
error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` closure
|
||||
error[E0507]: cannot move out of `children`, a captured variable in an `Fn` closure
|
||||
```
|
||||
|
||||
The problem here is that both `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple times. The first time you construct `<Suspense/>`’s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` children construction.
|
||||
|
||||
## The Details
|
||||
|
||||
> Feel free to skip ahead to the solution.
|
||||
|
||||
If you want to really understand the issue here, it may help to look at the expanded `view` macro. Here’s a cleaned-up version:
|
||||
|
||||
```rust
|
||||
Suspense(
|
||||
cx,
|
||||
::leptos::component_props_builder(&Suspense)
|
||||
.fallback(|| ())
|
||||
.children({
|
||||
// fallback and children are moved into this closure
|
||||
Box::new(move |cx| {
|
||||
{
|
||||
// fallback and children captured here
|
||||
leptos::Fragment::lazy(|| {
|
||||
vec![
|
||||
(Show(
|
||||
cx,
|
||||
::leptos::component_props_builder(&Show)
|
||||
.when(|| true)
|
||||
// but fallback is moved into Show here
|
||||
.fallback(fallback)
|
||||
// and children is moved into Show here
|
||||
.children(children)
|
||||
.build(),
|
||||
)
|
||||
.into_view(cx)),
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
```
|
||||
|
||||
All components own their props; so the `<Show/>` in this case can’t be called because it only has captured references to `fallback` and `children`.
|
||||
|
||||
## Solution
|
||||
|
||||
However, both `<Suspense/>` and `<Show/>` take `ChildrenFn`, i.e., their `children` should implement the `Fn` type so they can be called multiple times with only an immutable reference. This means we don’t need to own `children` or `fallback`; we just need to be able to pass `'static` references to them.
|
||||
|
||||
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods.
|
||||
|
||||
In this case, it’s really simple:
|
||||
|
||||
```rust
|
||||
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
|
||||
where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
let fallback = store_value(cx, fallback);
|
||||
let children = store_value(cx, children);
|
||||
view! { cx,
|
||||
<Suspense
|
||||
fallback=|| ()
|
||||
>
|
||||
<Show
|
||||
when=|| todo!()
|
||||
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
|
||||
>
|
||||
{children.with_value(|children| children(cx))}
|
||||
</Show>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `<Show/>` component and call them there.
|
||||
|
||||
## A Final Note
|
||||
|
||||
Note that this works because `<Show/>` and `<Suspense/>` only need an immutable reference to their children (which `.with_value` can give it), not ownership.
|
||||
|
||||
In other cases, you may need to project owned props through a function that takes `ChildrenFn` and therefore needs to be called more than once. In this case, you may find the `clone:` helper in the`view` macro helpful.
|
||||
|
||||
Consider this example
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let name = "Alice".to_string();
|
||||
view! { cx,
|
||||
<Outer>
|
||||
<Inner>
|
||||
<Inmost name=name.clone()/>
|
||||
</Inner>
|
||||
</Outer>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
|
||||
children(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
|
||||
view! { cx,
|
||||
<p>{name}</p>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Even with `name=name.clone()`, this gives the error
|
||||
|
||||
```
|
||||
cannot move out of `name`, a captured variable in an `Fn` closure
|
||||
```
|
||||
|
||||
It’s captured through multiple levels of children that need to run more than once, and there’s no obvious way to clone it _into_ the children.
|
||||
|
||||
In this case, the `clone:` syntax comes in handy. Calling `clone:name` will clone `name` _before_ moving it into `<Inner/>`’s children, which solves our ownership issue.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Outer>
|
||||
<Inner clone:name>
|
||||
<Inmost name=name.clone()/>
|
||||
</Inner>
|
||||
</Outer>
|
||||
}
|
||||
```
|
||||
|
||||
These issues can be a little tricky to understand or debug, because of the opacity of the `view` macro. But in general, they can always be solved.
|
||||
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 has 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!
|
||||
@@ -23,8 +23,6 @@ async fn fetch_results() {
|
||||
// some async function to fetch our search results
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Search(cx: Scope) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn FormExample(cx: Scope) -> impl IntoView {
|
||||
// reactive access to URL query strings
|
||||
|
||||
125
docs/book/src/server/25_server_functions.md
Normal file
125
docs/book/src/server/25_server_functions.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Server Functions
|
||||
|
||||
If you’re creating anything beyond a toy app, you’ll need to run code on the server all the time: reading from or writing to a database that only runs on the server, running expensive computations using libraries you don’t want to ship down to the client, accessing APIs that need to be called from the server rather than the client for CORS reasons or because you need a secret API key that’s stored on the server and definitely shouldn’t be shipped down to a user’s browser.
|
||||
|
||||
Traditionally, this is done by separating your server and client code, and by setting up something like a REST API or GraphQL API to allow your client to fetch and mutate data on the server. This is fine, but it requires you to write and maintain your code in multiple separate places (client-side code for fetching, server-side functions to run), as well as creating a third thing to manage, which is the API contract between the two.
|
||||
|
||||
Leptos is one of a number of modern frameworks that introduce the concept of **server functions**. Server functions have two key characteristics:
|
||||
|
||||
1. Server functions are **co-located** with your component code, so that you can organize your work by feature, not by technology. For example, you might have a “dark mode” feature that should persist a user’s dark/light mode preference across sessions, and be applied during server rendering so there’s no flicker. This requires a component that needs to be interactive on the client, and some work to be done on the server (setting a cookie, maybe even storing a user in a database.) Traditionally, this feature might end up being split between two different locations in your code, one in your “frontend” and one in your “backend.” With server functions, you’ll probably just write them both in one `dark_mode.rs` and forget about it.
|
||||
2. Server functions are **isomorphic**, i.e., they can be called either from the server or the browser. This is done by generating code differently for the two platforms. On the server, a server function simply runs. In the browser, the server function’s body is replaced with a stub that actually makes a fetch request to the server, serializing the arguments into the request and deserializing the return value from the response. But on either end, the function can simply be called: you can create an `add_todo` function that writes to your database, and simply call it from a click handler on a button in the browser!
|
||||
|
||||
## Using Server Functions
|
||||
|
||||
Actually, I kind of like that example. What would it look like? It’s pretty simple, actually.
|
||||
|
||||
```rust
|
||||
// todo.rs
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BusyButton(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<button on:click=move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
});
|
||||
}>
|
||||
"Add Todo"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
// somewhere in main.rs
|
||||
fn main() {
|
||||
// ...
|
||||
|
||||
AddTodo::register();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You’ll notice a couple things here right away:
|
||||
|
||||
- Server functions can use server-only dependencies, like `sqlx`, and can access server-only resources, like our database.
|
||||
- Server functions are `async`. Even if they only did synchronous work on the server, the function signature would still need to be `async`, because calling them from the browser _must_ be asynchronous.
|
||||
- Server functions return `Result<T, ServerFnError>`. Again, even if they only do infallible work on the server, this is true, because `ServerFnError`’s variants include the various things that can be wrong during the process of making a network request.
|
||||
- Server functions can be called from the client. Take a look at our click handler. This is code that will _only ever_ run on the client. But it can call the function `add_todo` (using `spawn_local` to run the `Future`) as if it were an ordinary async function:
|
||||
|
||||
```rust
|
||||
move |_| {
|
||||
spawn_local(async {
|
||||
add_todo("So much to do!".to_string()).await;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- Server functions are top-level functions defined with `fn`. Unlike event listeners, derived signals, and most everything else in Leptos, they are not closures! As `fn` calls, they have no access to the reactive state of your app or anything else that is not passed in as an argument. And again, this makes perfect sense: When you make a request to the server, the server doesn’t have access to client state unless you send it explicitly. (Otherwise we’d have to serialize the whole reactive system and send it across the wire with every request, which—while it served classic ASP for a while—is a really bad idea.)
|
||||
- Server function arguments and return values both need to be serializable with `serde`. Again, hopefully this makes sense: while function arguments in general don’t need to be serialized, calling a server function from the browser means serializing the arguments and sending them over HTTP.
|
||||
|
||||
There are a few things to note about the way you define a server function, too.
|
||||
|
||||
- Server functions are created by using the [`#[server]` macro](https://docs.rs/leptos_server/latest/leptos_server/index.html#server) to annotate a top-level function, which can be defined anywhere.
|
||||
- We provide the macro a type name. The type name is used to register the server function (in `main.rs`), and it’s used internally as a container to hold, serialize, and deserialize the arguments.
|
||||
- We provide the macro a path. This is a prefix for the path at which we’ll mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
|
||||
- You’ll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
|
||||
|
||||
## Server Function Encodings
|
||||
|
||||
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which we’ll see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
|
||||
|
||||
```rust
|
||||
#[server(AddTodo, "/api", "Url")]
|
||||
#[server(AddTodo, "/api", "GetJson")]
|
||||
#[server(AddTodo, "/api", "Cbor")]
|
||||
#[server(AddTodo, "/api", "GetCbor")]
|
||||
```
|
||||
|
||||
The four options use different combinations of HTTP verbs and encoding methods:
|
||||
|
||||
| Name | Method | Request | Response |
|
||||
| ----------------- | ------ | ----------- | -------- |
|
||||
| **Url** (default) | POST | URL encoded | JSON |
|
||||
| **GetJson** | GET | URL encoded | JSON |
|
||||
| **Cbor** | POST | CBOR | CBOR |
|
||||
| **GetCbor** | GET | URL encoded | CBOR |
|
||||
|
||||
In other words, you have two choices:
|
||||
|
||||
- `GET` or `POST`? This has implications for things like browser or CDN caching; while `POST` requests should not be cached, `GET` requests can be.
|
||||
- Plain text (arguments sent with URL/form encoding, results sent as JSON) or a binary format (CBOR, encoded as a base64 string)?
|
||||
|
||||
**But remember**: Leptos will handle all the details of this encoding and decoding for you. When you use a server function, it looks just like calling any other asynchronous function!
|
||||
|
||||
## An Important Note on Security
|
||||
|
||||
Server functions are a cool technology, but it’s very important to remember. **Server functions are not magic; they’re syntax sugar for defining a public API.** The _body_ of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and it’s return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.
|
||||
|
||||
## Integrating Server Functions with Leptos
|
||||
|
||||
So far, everything I’ve said is actually framework agnostic. (And in fact, the Leptos server function crate has been integrated into Dioxus as well!) Server functions are simply a way of defining a function-like RPC call that leans on Web standards like HTTP requests and URL encoding.
|
||||
|
||||
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](../async/README.md). So you can easily integrate your server functions with the rest of your applications:
|
||||
|
||||
- Create **resources** that call the server function to load data from the server
|
||||
- Read these resources under `<Suspense/>` or `<Transition/>` to enable streaming SSR and fallback states while data loads.
|
||||
- Create **actions** that call the server function to mutate data on the server
|
||||
|
||||
The final section of this book will make this a little more concrete by introducing patterns that use progressively-enhanced HTML forms to run these server actions.
|
||||
|
||||
But in the next few chapters, we’ll actually take a look at some of the details of what you might want to do with your server functions, including the best ways to integrate with the powerful extractors provided by the Actix and Axum server frameworks.
|
||||
11
docs/book/src/server/README.md
Normal file
11
docs/book/src/server/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Working with the Server
|
||||
|
||||
The previous section described the process of server-side rendering, using the server to generate an HTML version of the page that will become interactive in the browser. So far, everything has been “isomorphic” or “universal”; in other words, your app has had the “same (_iso_) shape (_morphe_)” on the client and the server.
|
||||
|
||||
But a server can do a lot more than just render HTML! In fact, a server can do a whole bunch of things your browser _can’t,_ like reading from and writing to a SQL database.
|
||||
|
||||
If you’re used to building JavaScript frontend apps, you’re probably used to calling out to some kind of REST API to do this sort of server work. If you’re used to building sites with PHP or Python or Ruby (or Java or C# or...), this server-side work is your bread and butter, and it’s the client-side interactivity that tends to be an afterthought.
|
||||
|
||||
With Leptos, you can do both: not only in the same language, not only sharing the same types, but even in the same files!
|
||||
|
||||
This section will talk about how to build the uniquely-server-side parts of your application.
|
||||
37
docs/book/src/ssr/21_cargo_leptos.md
Normal file
37
docs/book/src/ssr/21_cargo_leptos.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Introducing `cargo-leptos`
|
||||
|
||||
So far, we’ve just been running code in the browser and using Trunk to coordinate the build process and run a local development process. If we’re going to add server-side rendering, we’ll need to run our application code on the server as well. This means we’ll need to build two separate binaries, one compiled to native code and running the server, the other compiled to WebAssembly (WASM) and running in the user’s browser. Additionally, the server needs to know how to serve this WASM version (and the JavaScript required to initialize it) to the browser.
|
||||
|
||||
This is not an insurmountable task but it adds some complication. For convenience and an easier developer experience, we built the [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) build tool. `cargo-leptos` basically exists to coordinate the build process for your app, handling recompiling the server and client halves when you make changes, and adding some built-in support for things like Tailwind, SASS, and testing.
|
||||
|
||||
Getting started is pretty easy. Just run
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
```
|
||||
|
||||
And then to create a new project, you can run either
|
||||
|
||||
```bash
|
||||
# for an Actix template
|
||||
cargo leptos new --git leptos-rs/start
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
# for an Axum template
|
||||
cargo leptos new --git leptos-rs/start-axum
|
||||
```
|
||||
|
||||
Now `cd` into the directory you’ve created and run
|
||||
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Once your app has compiled you can open up your browser to [`http://localhost:3000`](http://localhost:3000) to see it.
|
||||
|
||||
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/cargo-leptos/blob/main/README.md).
|
||||
|
||||
But what exactly is happening when you open our browser to `localhost:3000`? Well, read on to find out.
|
||||
43
docs/book/src/ssr/22_life_cycle.md
Normal file
43
docs/book/src/ssr/22_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/23_ssr_modes.md
Normal file
122
docs/book/src/ssr/23_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.
|
||||
148
docs/book/src/ssr/24_hydration_bugs.md
Normal file
148
docs/book/src/ssr/24_hydration_bugs.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Hydration Bugs _(and how to avoid them)_
|
||||
|
||||
## A Thought Experiment
|
||||
|
||||
Let’s try an experiment to test your intuitions. Open up an app you’re server-rendering with `cargo-leptos`. (If you’ve just been using `trunk` so far to play with examples, go [clone a `cargo-leptos` template](./21_cargo_leptos.md) just for the sake of this exercise.)
|
||||
|
||||
Put a log somewhere in your root component. (I usually call mine `<App/>`, but anything will do.)
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
leptos::log!("where do I run?");
|
||||
// ... whatever
|
||||
}
|
||||
```
|
||||
|
||||
And let’s fire it up
|
||||
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Where do you expect `where do I run?` to log?
|
||||
|
||||
- In the command line where you’re running the server?
|
||||
- In the browser console when you load the page?
|
||||
- Neither?
|
||||
- Both?
|
||||
|
||||
Try it out.
|
||||
|
||||
...
|
||||
|
||||
...
|
||||
|
||||
...
|
||||
|
||||
Okay, consider the spoiler alerted.
|
||||
|
||||
You’ll notice of course that it logs in both places, assuming everything goes according to plan. In fact on the server it logs twice—first during the initial server startup, when Leptos renders your app once to extract the route tree, then a second time when you make a request. Each time you reload the page, `where do I run?` should log once on the server and once on the client.
|
||||
|
||||
If you think about the description in the last couple sections, hopefully this makes sense. Your application runs once on the server, where it builds up a tree of HTML which is sent to the client. During this initial render, `where do I run?` logs on the server.
|
||||
|
||||
Once the WASM binary has loaded in the browser, your application runs a second time, walking over the same user interface tree and adding interactivity.
|
||||
|
||||
> Does that sound like a waste? It is, in a sense. But reducing that waste is a genuinely hard problem. It’s what some JS frameworks like Qwik are intended to solve, although it’s probably too early to tell whether it’s a net performance gain as opposed to other approaches.
|
||||
|
||||
## The Potential for Bugs
|
||||
|
||||
Okay, hopefully all of that made sense. But what does it have to do with the title of this chapter, which is “Hydration bugs (and how to avoid them)”?
|
||||
|
||||
Remember that the application needs to run on both the server and the client. This generates a few different sets of potential issues you need to know how to avoid.
|
||||
|
||||
### Mismatches between server and client code
|
||||
|
||||
One way to create a bug is by creating a mismatch between the HTML that’s sent down by the server and what’s rendered on the client. It’s actually fairly hard to do this unintentionally, I think (at least judging by the bug reports I get from people.) But imagine I do something like this
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let data = if cfg!(target_arch = "wasm32") {
|
||||
vec![0, 1, 2]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
data.into_iter()
|
||||
.map(|value| view! { cx, <span>{value}</span> })
|
||||
.collect_view(cx)
|
||||
}
|
||||
```
|
||||
|
||||
In other words, if this is being compiled to WASM, it has three items; otherwise it’s empty.
|
||||
|
||||
When I load the page in the browser, I see nothing. If I open the console I see a bunch of warnings:
|
||||
|
||||
```
|
||||
element with id 0-0-1 not found, ignoring it for hydration
|
||||
element with id 0-0-2 not found, ignoring it for hydration
|
||||
element with id 0-0-3 not found, ignoring it for hydration
|
||||
component with id _0-0-4c not found, ignoring it for hydration
|
||||
component with id _0-0-4o not found, ignoring it for hydration
|
||||
```
|
||||
|
||||
The WASM version of your app, running in the browser, expects to find three items; but the HTML has none.
|
||||
|
||||
#### Solution
|
||||
|
||||
It’s pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If you’re seeing warnings like this and you don’t think it’s your fault, it’s much more likely that it’s a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
|
||||
|
||||
### Not all client code can run on the server
|
||||
|
||||
Imagine you happily import a dependency like `gloo-net` that you’ve been used to using to make requests in the browser, and use it in a `create_resource` in a server-rendered app.
|
||||
|
||||
You’ll probably instantly see the dreaded message
|
||||
|
||||
```
|
||||
panicked at 'cannot call wasm-bindgen imported functions on non-wasm targets'
|
||||
```
|
||||
|
||||
Uh-oh.
|
||||
|
||||
But of course this makes sense. We’ve just said that your app needs to run on the client and the server.
|
||||
|
||||
#### Solution
|
||||
|
||||
There are a few ways to avoid this:
|
||||
|
||||
1. Only use libraries that can run on both the server and the client. `reqwest`, for example, works for making HTTP requests in both settings.
|
||||
2. Use different libraries on the server and the client, and gate them using the `#[cfg]` macro. ([Click here for an example](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).)
|
||||
3. Wrap client-only code in `create_effect`. Because `create_effect` only runs on the client, this can be an effective way to access browser APIs that are not needed for initial rendering.
|
||||
|
||||
For example, say that I want to store something in the browser’s `localStorage` whenever a signal changes.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
}
|
||||
```
|
||||
|
||||
This panics because I can’t access `LocalStorage` during server rendering.
|
||||
|
||||
But if I wrap it in an effect...
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
use gloo_storage::Storage;
|
||||
create_effect(cx, move |_| {
|
||||
let storage = gloo_storage::LocalStorage::raw();
|
||||
leptos::log!("{storage:?}");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
It’s fine! This will render appropriately on the server, ignoring the client-only code, and then access the storage and log a message on the browser.
|
||||
|
||||
### Not all server code can run on the client
|
||||
|
||||
WebAssembly running in the browser is a pretty limited environment. You don’t have access to a file-system or to many of the other things the standard library may be used to having. Not every crate can even be compiled to WASM, let alone run in a WASM environment.
|
||||
|
||||
In particular, you’ll sometimes see errors about the crate `mio` or missing things from `core`. This is generally a sign that you are trying to compile something to WASM that can’t be compiled to WASM. If you’re adding server-only dependencies, you’ll want to mark them `optional = true` in your `Cargo.toml` and then enable them in the `ssr` feature definition. (Check out one of the template `Cargo.toml` files to see more details.)
|
||||
|
||||
You can use `create_effect` to specify that something should only run on the client, and not in the server. Is there a way to specify that something should run only on the server, and not the client?
|
||||
|
||||
In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html).)
|
||||
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,10 +1,10 @@
|
||||
# `view`: Dynamic Attributes and Classes
|
||||
# `view`: Dynamic Classes, Styles and Attributes
|
||||
|
||||
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 classes, styles and attributes 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
|
||||
@@ -52,6 +52,42 @@ reactively update when the signal changes.
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
the number switches between even and odd.
|
||||
|
||||
Some CSS class names can’t be directly parsed by the `view` macro, especially if they include a mix of dashes and numbers or other characters. In that case, you can use a tuple syntax: `class=("name", value)` still directly updates a single class.
|
||||
|
||||
```rust
|
||||
class=("button-20", move || count() % 2 == 1)
|
||||
```
|
||||
|
||||
> If you’re following along, make sure you go into your `index.html` and add something like this:
|
||||
>
|
||||
> ```html
|
||||
> <style>
|
||||
> .red {
|
||||
> color: red;
|
||||
> }
|
||||
> </style>
|
||||
> ```
|
||||
|
||||
## Dynamic Styles
|
||||
|
||||
Individual CSS properties can be directly updated with a similar `style:` syntax.
|
||||
|
||||
```rust
|
||||
let (x, set_x) = create_signal(cx, 0);
|
||||
let (y, set_y) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
<div
|
||||
style="position: absolute"
|
||||
style:left=move || format!("{}px", x() + 100)
|
||||
style:top=move || format!("{}px", y() + 100)
|
||||
style:background-color=move || format!("rgb({}, {}, 100)", x(), y())
|
||||
style=("--columns", x)
|
||||
>
|
||||
"Moves when coordinates change"
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Attributes
|
||||
|
||||
The same applies to plain attributes. Passing a plain string or primitive value to
|
||||
|
||||
@@ -106,6 +106,9 @@ in as if it were an HTML element attribute. Simple.
|
||||
> 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
|
||||
|
||||
|
||||
@@ -31,6 +31,22 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
|
||||
|
||||
```rust
|
||||
let values = vec![0, 1, 2];
|
||||
view! { cx,
|
||||
// this will just render "012"
|
||||
<p>{values.clone()}</p>
|
||||
// or we can wrap them in <li>
|
||||
<ul>
|
||||
{values.into_iter()
|
||||
.map(|n| view! { cx, <li>{n}</li>})
|
||||
.collect_view(cx)}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
|
||||
The fact that the _list_ is static doesn’t mean the interface needs to be static.
|
||||
You can render dynamic items as part of a static list.
|
||||
|
||||
@@ -52,7 +68,7 @@ let counter_buttons = counters
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect_view(cx);
|
||||
|
||||
view! { cx,
|
||||
<ul>{counter_buttons}</ul>
|
||||
|
||||
@@ -80,7 +80,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.collect::<Vec<_>>();
|
||||
.collect_view(cx);
|
||||
|
||||
view! { cx,
|
||||
<ul>{children}</ul>
|
||||
|
||||
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.
57
examples/Makefile.toml
Normal file
57
examples/Makefile.toml
Normal file
@@ -0,0 +1,57 @@
|
||||
extend = [{ path = "./cargo-make/common.toml" }]
|
||||
|
||||
[env]
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
|
||||
CARGO_MAKE_WORKSPACE_EMULATION = true
|
||||
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
|
||||
"counter",
|
||||
"counter_isomorphic",
|
||||
"counters",
|
||||
"counters_stable",
|
||||
"counter_without_macros",
|
||||
"error_boundary",
|
||||
"errors_axum",
|
||||
"fetch",
|
||||
"hackernews",
|
||||
"hackernews_axum",
|
||||
"login_with_token_csr_only",
|
||||
"parent_child",
|
||||
"router",
|
||||
"session_auth_axum",
|
||||
"ssr_modes",
|
||||
"ssr_modes_axum",
|
||||
"tailwind",
|
||||
"tailwind_csr_trunk",
|
||||
"todo_app_sqlite",
|
||||
"todo_app_sqlite_axum",
|
||||
"todo_app_sqlite_viz",
|
||||
"todomvc",
|
||||
]
|
||||
|
||||
[tasks.verify-flow]
|
||||
description = "Provides pre and post hooks for verify"
|
||||
dependencies = ["pre-verify", "verify", "post-verify"]
|
||||
|
||||
[tasks.verify]
|
||||
description = "Run all quality checks and tests"
|
||||
dependencies = ["check-style", "test-unit-and-web"]
|
||||
|
||||
[tasks.test-unit-and-web]
|
||||
description = "Run all unit and web tests"
|
||||
dependencies = ["test-flow", "web-test-flow"]
|
||||
|
||||
[tasks.pre-verify]
|
||||
|
||||
[tasks.post-verify]
|
||||
dependencies = ["clean-all"]
|
||||
|
||||
[tasks.web-test-flow]
|
||||
description = "Provides pre and post hooks for web-test"
|
||||
dependencies = ["pre-web-test", "web-test", "post-web-test"]
|
||||
|
||||
[tasks.pre-web-test]
|
||||
|
||||
[tasks.web-test]
|
||||
|
||||
[tasks.post-web-test]
|
||||
23
examples/cargo-make/common.toml
Normal file
23
examples/cargo-make/common.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[tasks.pre-clippy]
|
||||
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
|
||||
|
||||
[tasks.check-style]
|
||||
description = "Check for style violations"
|
||||
dependencies = ["check-format-flow", "clippy-flow"]
|
||||
|
||||
[tasks.verify-local]
|
||||
description = "Run all quality checks and tests from an example directory"
|
||||
dependencies = ["check-style", "test-local"]
|
||||
|
||||
[tasks.test-local]
|
||||
description = "Run all tests from an example directory"
|
||||
dependencies = ["test", "web-test"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
description = "Runs the trunk clean command."
|
||||
category = "Cleanup"
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean", "clean-trunk"]
|
||||
4
examples/cargo-make/wasm-web-test.toml
Normal file
4
examples/cargo-make/wasm-web-test.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[tasks.web-test]
|
||||
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
|
||||
command = "cargo"
|
||||
args = ["make", "wasm-pack-test"]
|
||||
@@ -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,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use leptos::*;
|
||||
|
||||
/// A simple counter component.
|
||||
///
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleCounter(
|
||||
@@ -9,7 +9,7 @@ pub fn SimpleCounter(
|
||||
/// The starting value for the counter
|
||||
initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
step: i32
|
||||
step: i32,
|
||||
) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use counter::*;
|
||||
use counter::SimpleCounter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx,
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
mount_to_body(|cx| {
|
||||
view! { cx,
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ wasm_bindgen_test_configure!(run_in_browser);
|
||||
fn clear() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
let _ = document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
// start by rendering our counter and mounting it to the DOM
|
||||
// note that we start at the initial value of 10
|
||||
@@ -38,7 +38,7 @@ fn clear() {
|
||||
// test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let (value, _set_value) = create_signal(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
@@ -71,7 +71,7 @@ fn clear() {
|
||||
fn inc() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
let _ = document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
@@ -79,7 +79,7 @@ fn inc() {
|
||||
);
|
||||
|
||||
// You can do testing with vanilla DOM operations
|
||||
let document = leptos::document();
|
||||
let _document = leptos::document();
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use broadcaster::BroadcastChannel;
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use broadcaster::BroadcastChannel;
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register_server_functions() {
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
pub fn register_server_functions() {
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
|
||||
#[server(GetServerCount, "/api")]
|
||||
pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
@@ -29,7 +29,10 @@ pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
}
|
||||
|
||||
#[server(AdjustServerCount, "/api")]
|
||||
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
|
||||
pub async fn adjust_server_count(
|
||||
delta: i32,
|
||||
msg: String,
|
||||
) -> Result<i32, ServerFnError> {
|
||||
let new = COUNT.load(Ordering::Relaxed) + delta;
|
||||
COUNT.store(new, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&new).await;
|
||||
@@ -46,36 +49,49 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
<p>"Each of these counters stores its data in the same variable on the server."</p>
|
||||
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
|
||||
<p>
|
||||
"The value is shared across connections. Try opening this is another browser tab to see what I mean."
|
||||
</p>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><A href="">"Simple"</A></li>
|
||||
<li><A href="form">"Form-Based"</A></li>
|
||||
<li><A href="multi">"Multi-User"</A></li>
|
||||
<li>
|
||||
<A href="">"Simple"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="form">"Form-Based"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="multi">"Multi-User"</A>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Counter/>
|
||||
}/>
|
||||
<Route path="form" view=|cx| view! {
|
||||
cx,
|
||||
<FormCounter/>
|
||||
}/>
|
||||
<Route path="multi" view=|cx| view! {
|
||||
cx,
|
||||
<MultiuserCounter/>
|
||||
}/>
|
||||
<Route
|
||||
path=""
|
||||
view=|cx| {
|
||||
view! { cx, <Counter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="form"
|
||||
view=|cx| {
|
||||
view! { cx, <FormCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="multi"
|
||||
view=|cx| {
|
||||
view! { cx, <MultiuserCounter/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -93,33 +109,47 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || (dec.version().get(), inc.version().get(), clear.version().get()),
|
||||
move || {
|
||||
(
|
||||
dec.version().get(),
|
||||
inc.version().get(),
|
||||
clear.version().get(),
|
||||
)
|
||||
},
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let error_msg = move || {
|
||||
let value = move || {
|
||||
counter
|
||||
.read(cx)
|
||||
.map(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
.flatten()
|
||||
.map(|count| count.unwrap_or(0))
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let error_msg = move || {
|
||||
counter.read(cx).and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Simple Counter"</h2>
|
||||
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
|
||||
<p>
|
||||
"This counter sets the value on the server and automatically reloads the new value."
|
||||
</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
|
||||
{move || {
|
||||
error_msg()
|
||||
.map(|msg| {
|
||||
view! { cx, <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -142,19 +172,15 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let value = move || {
|
||||
log::debug!("FormCounter looking for value");
|
||||
counter
|
||||
.read(cx)
|
||||
.map(|n| n.ok())
|
||||
.flatten()
|
||||
.map(|n| n)
|
||||
.unwrap_or(0)
|
||||
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Form Counter"</h2>
|
||||
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
|
||||
<p>
|
||||
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
|
||||
</p>
|
||||
<div>
|
||||
// calling a server function is the same as POSTing to its API URL
|
||||
// so we can just do that with a form and button
|
||||
@@ -185,26 +211,32 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
// This is the primitive pattern for live chat, collaborative editing, etc.
|
||||
#[component]
|
||||
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let dec =
|
||||
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let mut source =
|
||||
gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source.subscribe("message").unwrap().map(|value| {
|
||||
match value {
|
||||
Ok(value) => {
|
||||
value.1.data().as_string().expect("expected string value")
|
||||
},
|
||||
source
|
||||
.subscribe("message")
|
||||
.unwrap()
|
||||
.map(|value| match value {
|
||||
Ok(value) => value
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect("expected string value"),
|
||||
Err(_) => "0".to_string(),
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
@@ -212,18 +244,20 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let (multiplayer_value, _) =
|
||||
create_signal(cx, None::<i32>);
|
||||
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<h2>"Multi-User Counter"</h2>
|
||||
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
|
||||
<p>
|
||||
"This one uses server-sent events (SSE) to live-update when other users make changes."
|
||||
</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default().to_string()}</span>
|
||||
<span>
|
||||
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
|
||||
</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod counters;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::counters::*;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod counters;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::*;
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
@@ -37,7 +37,7 @@ cfg_if! {
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
@@ -48,7 +48,7 @@ cfg_if! {
|
||||
.service(counter_events)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
|
||||
.service(Files::new("/", &site_root))
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
|
||||
@@ -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,11 @@ 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"
|
||||
rstest = "0.17.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = ["HtmlElement", "XPathResult"]
|
||||
version = "0.3.61"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[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.
|
||||
|
||||
@@ -2,8 +2,8 @@ use leptos::{ev, html::*, *};
|
||||
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, Count::new(initial_value, step));
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
@@ -16,13 +16,13 @@ pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_value.update(|value| *value = 0))
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value -= step)
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child("-1"),
|
||||
)
|
||||
@@ -31,14 +31,45 @@ pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child((cx, move || value.get()))
|
||||
.child(move || count.get().value())
|
||||
.child("!"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value += step)
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.child("+1"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Count {
|
||||
value: i32,
|
||||
step: i32,
|
||||
}
|
||||
|
||||
impl Count {
|
||||
pub fn new(value: i32, step: u32) -> Self {
|
||||
Count {
|
||||
value,
|
||||
step: step as i32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> i32 {
|
||||
self.value
|
||||
}
|
||||
|
||||
pub fn increase(&mut self) {
|
||||
self.value += self.step;
|
||||
}
|
||||
|
||||
pub fn decrease(&mut self) {
|
||||
self.value += -self.step;
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
49
examples/counter_without_macros/tests/business.rs
Normal file
49
examples/counter_without_macros/tests/business.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
mod count {
|
||||
use counter_without_macros::Count;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(-2, 1)]
|
||||
#[case(-1, 1)]
|
||||
#[case(0, 1)]
|
||||
#[case(1, 1)]
|
||||
#[case(2, 1)]
|
||||
#[case(3, 2)]
|
||||
#[case(4, 3)]
|
||||
fn should_increase_count(#[case] initial_value: i32, #[case] step: u32) {
|
||||
let mut count = Count::new(initial_value, step);
|
||||
count.increase();
|
||||
assert_eq!(count.value(), initial_value + step as i32);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(-2, 1)]
|
||||
#[case(-1, 1)]
|
||||
#[case(0, 1)]
|
||||
#[case(1, 1)]
|
||||
#[case(2, 1)]
|
||||
#[case(3, 2)]
|
||||
#[case(4, 3)]
|
||||
#[trace]
|
||||
fn should_decrease_count(#[case] initial_value: i32, #[case] step: u32) {
|
||||
let mut count = Count::new(initial_value, step);
|
||||
count.decrease();
|
||||
assert_eq!(count.value(), initial_value - step as i32);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(-2, 1)]
|
||||
#[case(-1, 1)]
|
||||
#[case(0, 1)]
|
||||
#[case(1, 1)]
|
||||
#[case(2, 1)]
|
||||
#[case(3, 2)]
|
||||
#[case(4, 3)]
|
||||
#[trace]
|
||||
fn should_clear_count(#[case] initial_value: i32, #[case] step: u32) {
|
||||
let mut count = Count::new(initial_value, step);
|
||||
count.clear();
|
||||
assert_eq!(count.value(), 0);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -11,4 +11,5 @@ console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = "0.3"
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/common.toml" },
|
||||
{ path = "../cargo-make/wasm-web-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{For, ForProps, *};
|
||||
use leptos::{For, *};
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
@@ -72,7 +72,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use counters::{Counters, CountersProps};
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
use counters::{Counters, CountersProps};
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let add_counter = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
// add 3 counters
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
|
||||
// check HTML
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>0</span> from <span>3</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>0</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
let counters = div
|
||||
.query_selector("ul")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.children();
|
||||
|
||||
// click first counter once, second counter twice, etc.
|
||||
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let inc_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
for _ in 0..=idx {
|
||||
inc_button.click();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>6</span> from <span>3</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>1</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>2</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>3</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
// remove the first counter
|
||||
counters
|
||||
.item(0)
|
||||
.unwrap()
|
||||
.last_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.click();
|
||||
|
||||
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span>5</span> from <span>2</span> counters.</p><ul><li><button>-1</button><input type=\"text\"><span>2</span><button>+1</button><button>x</button></li><li><button>-1</button><input type=\"text\"><span>3</span><button>+1</button><button>x</button></li></ul>");
|
||||
|
||||
// decrement all by 1
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let dec_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
dec_button.click();
|
||||
}
|
||||
|
||||
run_scope(create_runtime(), move |cx| {
|
||||
// we can use RSX in test comparisons!
|
||||
// note that if RSX template creation is bugged, this probably won't catch it
|
||||
// (because the same bug will be reproduced in both sides of the assertion)
|
||||
// so I use HTML tests for most internal testing like this
|
||||
// but in user-land testing, RSX comparanda are cool
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Add Counter"</button>
|
||||
<button>"Add 1000 Counters"</button>
|
||||
<button>"Clear Counters"</button>
|
||||
<p>"Total: "<span>"3"</span>" from "<span>"2"</span>" counters."</p>
|
||||
<ul>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"1"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"2"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
.outer_html()
|
||||
);
|
||||
});
|
||||
}
|
||||
120
examples/counters/tests/web.rs
Normal file
120
examples/counters/tests/web.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| view! { cx, <Counters/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let add_counter = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
// add 3 counters
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
|
||||
// check HTML
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->0<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
|
||||
let counters = div
|
||||
.query_selector("ul")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.children();
|
||||
|
||||
// click first counter once, second counter twice, etc.
|
||||
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
|
||||
for idx in 0..counters.length() {
|
||||
let counter = counters.item(idx).unwrap();
|
||||
let inc_button = counter
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>();
|
||||
for _ in 0..=idx {
|
||||
inc_button.click();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->6<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->1<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->2<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->3<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
|
||||
// remove the first counter
|
||||
counters
|
||||
.item(0)
|
||||
.unwrap()
|
||||
.last_child()
|
||||
.unwrap()
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.click();
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->5<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->2<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->3<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
.collect_view(cx)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -10,12 +10,12 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -3,7 +3,7 @@ use cfg_if::cfg_if;
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -11,10 +11,10 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::landing::{App, AppProps};
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::landing::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
error_template::{ErrorTemplate, ErrorTemplateProps},
|
||||
errors::AppError,
|
||||
};
|
||||
use crate::{error_template::ErrorTemplate, errors::AppError};
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
@@ -54,7 +51,8 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
|
||||
let generate_internal_error =
|
||||
create_server_action::<CauseInternalServerError>(cx);
|
||||
|
||||
view! { cx,
|
||||
<p>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
@@ -8,6 +7,7 @@ pub mod landing;
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::landing::*;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::landing::*;
|
||||
use axum::body::Body as AxumBody;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
extract::{State, Path},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
@@ -21,7 +21,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
Extension(options): Extension<Arc<LeptosOptions>>,
|
||||
State(options): State<Arc<LeptosOptions>>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
@@ -37,13 +37,14 @@ async fn custom_handler(
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
crate::landing::register_server_functions();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
@@ -51,9 +52,13 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
|
||||
.leptos_routes(
|
||||
leptos_options.clone(),
|
||||
routes,
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -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,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -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,8 +54,8 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
errors.with(|errors| {
|
||||
errors
|
||||
.iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
|
||||
.collect_view(cx)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -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_view(cx)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -4,10 +4,7 @@ use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
mod routes;
|
||||
use routes::nav::*;
|
||||
use routes::stories::*;
|
||||
use routes::story::*;
|
||||
use routes::users::*;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
@@ -7,7 +7,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use hackernews::{App,AppProps};
|
||||
use hackernews::{App};
|
||||
use leptos_actix::{LeptosRoutes, generate_route_list};
|
||||
|
||||
#[get("/style.css")]
|
||||
@@ -24,7 +24,7 @@ cfg_if! {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
|
||||
@@ -37,7 +37,7 @@ cfg_if! {
|
||||
.service(favicon)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <App/> })
|
||||
.service(Files::new("/", &site_root))
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
@@ -46,7 +46,7 @@ cfg_if! {
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
use hackernews::{App, AppProps};
|
||||
use hackernews::{App};
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::api;
|
||||
|
||||
fn category(from: &str) -> &'static str {
|
||||
match from {
|
||||
"new" => "newest",
|
||||
@@ -37,8 +36,10 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link =
|
||||
move |cx| pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
let hide_more_link = move |cx| {
|
||||
pending()
|
||||
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
@@ -13,11 +13,20 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
|
||||
api::fetch_api::<api::Story>(
|
||||
cx,
|
||||
&api::story(&format!("item/{id}")),
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
|
||||
let meta_description = move || {
|
||||
story
|
||||
.read(cx)
|
||||
.and_then(|story| story.map(|story| story.title))
|
||||
.unwrap_or_else(|| "Loading story...".to_string())
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn User(cx: Scope) -> impl IntoView {
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
|
||||
<li inner_html={user.about} class="about"></li>
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
|
||||
@@ -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,3 +1,5 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{on_cleanup, Scope, Serializable};
|
||||
use leptos::{Scope, Serializable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
@@ -29,7 +29,7 @@ where
|
||||
|
||||
// abort in-flight requests if the Scope is disposed
|
||||
// i.e., if we've navigated away from this page
|
||||
on_cleanup(cx, move || {
|
||||
leptos::on_cleanup(cx, move || {
|
||||
if let Some(abort_controller) = abort_controller {
|
||||
abort_controller.abort()
|
||||
}
|
||||
@@ -38,7 +38,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
|
||||
pub async fn fetch_api<T>(_cx: Scope, path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
{
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use leptos::{
|
||||
signal_prelude::*, view, Errors, For, ForProps, IntoView, RwSignal, Scope,
|
||||
View,
|
||||
};
|
||||
use leptos::{view, Errors, For, IntoView, RwSignal, Scope, View};
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
|
||||
@@ -4,7 +4,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
@@ -15,13 +15,13 @@ if #[cfg(feature = "ssr")] {
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
|
||||
handler(req).await.into_response()
|
||||
|
||||
@@ -7,10 +7,7 @@ pub mod error_template;
|
||||
pub mod fallback;
|
||||
pub mod handlers;
|
||||
mod routes;
|
||||
use routes::nav::*;
|
||||
use routes::stories::*;
|
||||
use routes::story::*;
|
||||
use routes::users::*;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
@@ -7,7 +7,6 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
extract::Extension,
|
||||
};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
@@ -18,8 +17,8 @@ if #[cfg(feature = "ssr")] {
|
||||
use hackernews_axum::*;
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr.clone();
|
||||
let leptos_options = Arc::new(conf.leptos_options);
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
@@ -29,7 +28,7 @@ if #[cfg(feature = "ssr")] {
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{component, Scope, IntoView, view};
|
||||
use leptos::{component, view, IntoView, Scope};
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::api;
|
||||
|
||||
fn category(from: &str) -> &'static str {
|
||||
match from {
|
||||
"new" => "newest",
|
||||
@@ -37,8 +36,10 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
let hide_more_link = move || {
|
||||
pending()
|
||||
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
@@ -13,11 +13,20 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
|
||||
api::fetch_api::<api::Story>(
|
||||
cx,
|
||||
&api::story(&format!("item/{id}")),
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
|
||||
let meta_description = move || {
|
||||
story
|
||||
.read(cx)
|
||||
.and_then(|story| story.map(|story| story.title))
|
||||
.unwrap_or_else(|| "Loading story...".to_string())
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<>
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
extend = { path = "../cargo-make/common.toml" }
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
extend = { path = "../../cargo-make/common.toml" }
|
||||
1
examples/login_with_token_csr_only/client/Makefile.toml
Normal file
1
examples/login_with_token_csr_only/client/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../../cargo-make/common.toml" }
|
||||
@@ -1,9 +1,8 @@
|
||||
use api_boundary::*;
|
||||
use gloo_net::http::{Request, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use thiserror::Error;
|
||||
|
||||
use api_boundary::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct UnauthorizedApi {
|
||||
url: &'static str,
|
||||
|
||||
@@ -20,54 +20,56 @@ pub fn CredentialsForm(
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<form on:submit=|ev|ev.prevent_default()>
|
||||
<p>{ title }</p>
|
||||
{move || error.get().map(|err| view!{ cx,
|
||||
<p style ="color:red;" >{ err }</p>
|
||||
})}
|
||||
<input
|
||||
type = "email"
|
||||
required
|
||||
placeholder = "Email address"
|
||||
prop:disabled = move || disabled.get()
|
||||
on:keyup = move |ev: ev::KeyboardEvent| {
|
||||
let val = event_target_value(&ev);
|
||||
set_email.update(|v|*v = val);
|
||||
}
|
||||
// The `change` event fires when the browser fills the form automatically,
|
||||
on:change = move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_email.update(|v|*v = val);
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type = "password"
|
||||
required
|
||||
placeholder = "Password"
|
||||
prop:disabled = move || disabled.get()
|
||||
on:keyup = move |ev: ev::KeyboardEvent| {
|
||||
match &*ev.key() {
|
||||
"Enter" => {
|
||||
dispatch_action();
|
||||
<form on:submit=|ev| ev.prevent_default()>
|
||||
<p>{title}</p>
|
||||
{move || {
|
||||
error
|
||||
.get()
|
||||
.map(|err| {
|
||||
view! { cx, <p style="color:red;">{err}</p> }
|
||||
})
|
||||
}}
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
prop:disabled=move || disabled.get()
|
||||
on:keyup=move |ev: ev::KeyboardEvent| {
|
||||
let val = event_target_value(&ev);
|
||||
set_email.update(|v| *v = val);
|
||||
}
|
||||
_=> {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p|*p = val);
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_email.update(|v| *v = val);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The `change` event fires when the browser fills the form automatically,
|
||||
on:change = move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p|*p = val);
|
||||
}
|
||||
/>
|
||||
<button
|
||||
prop:disabled = move || button_is_disabled.get()
|
||||
on:click = move |_| dispatch_action()
|
||||
>
|
||||
{ action_label }
|
||||
</button>
|
||||
</form>
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
placeholder="Password"
|
||||
prop:disabled=move || disabled.get()
|
||||
on:keyup=move |ev: ev::KeyboardEvent| {
|
||||
match &*ev.key() {
|
||||
"Enter" => {
|
||||
dispatch_action();
|
||||
}
|
||||
_ => {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p| *p = val);
|
||||
}
|
||||
}
|
||||
}
|
||||
on:change=move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
set_password.update(|p| *p = val);
|
||||
}
|
||||
/>
|
||||
<button
|
||||
prop:disabled=move || button_is_disabled.get()
|
||||
on:click=move |_| dispatch_action()
|
||||
>
|
||||
{action_label}
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::Page;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::Page;
|
||||
|
||||
#[component]
|
||||
pub fn NavBar<F>(
|
||||
cx: Scope,
|
||||
@@ -13,20 +12,27 @@ where
|
||||
F: Fn() + 'static + Clone,
|
||||
{
|
||||
view! { cx,
|
||||
<nav>
|
||||
<Show
|
||||
when = move || logged_in.get()
|
||||
fallback = |cx| view! { cx,
|
||||
<A href=Page::Login.path() >"Login"</A>
|
||||
" | "
|
||||
<A href=Page::Register.path() >"Register"</A>
|
||||
}
|
||||
>
|
||||
<a href="#" on:click={
|
||||
let on_logout = on_logout.clone();
|
||||
move |_| on_logout()
|
||||
}>"Logout"</a>
|
||||
</Show>
|
||||
</nav>
|
||||
<nav>
|
||||
<Show
|
||||
when=move || logged_in.get()
|
||||
fallback=|cx| {
|
||||
view! { cx,
|
||||
<A href=Page::Login.path()>"Login"</A>
|
||||
" | "
|
||||
<A href=Page::Register.path()>"Register"</A>
|
||||
}
|
||||
}
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
on:click={
|
||||
let on_logout = on_logout.clone();
|
||||
move |_| on_logout()
|
||||
}
|
||||
>
|
||||
"Logout"
|
||||
</a>
|
||||
</Show>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use api_boundary::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use api_boundary::*;
|
||||
|
||||
mod api;
|
||||
mod components;
|
||||
mod pages;
|
||||
@@ -86,45 +85,51 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
.expect("LocalStorage::set");
|
||||
}
|
||||
None => {
|
||||
log::debug!("API is no longer authorized: delete token from LocalStorage");
|
||||
log::debug!(
|
||||
"API is no longer authorized: delete token from \
|
||||
LocalStorage"
|
||||
);
|
||||
LocalStorage::delete(API_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<Router>
|
||||
<NavBar logged_in on_logout />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=Page::Home.path()
|
||||
view=move |cx| view! { cx,
|
||||
<Home user_info = user_info.into() />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=Page::Login.path()
|
||||
view=move |cx| view! { cx,
|
||||
<Login
|
||||
api = unauthorized_api
|
||||
on_success = move |api| {
|
||||
log::info!("Successfully logged in");
|
||||
authorized_api.update(|v| *v = Some(api));
|
||||
let navigate = use_navigate(cx);
|
||||
navigate(Page::Home.path(), Default::default()).expect("Home route");
|
||||
fetch_user_info.dispatch(());
|
||||
} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=Page::Register.path()
|
||||
view=move |cx| view! { cx,
|
||||
<Register api = unauthorized_api />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
<Router>
|
||||
<NavBar logged_in on_logout/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=Page::Home.path()
|
||||
view=move |cx| {
|
||||
view! { cx, <Home user_info=user_info.into()/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=Page::Login.path()
|
||||
view=move |cx| {
|
||||
view! { cx,
|
||||
<Login
|
||||
api=unauthorized_api
|
||||
on_success=move |api| {
|
||||
log::info!("Successfully logged in");
|
||||
authorized_api.update(|v| *v = Some(api));
|
||||
let navigate = use_navigate(cx);
|
||||
navigate(Page::Home.path(), Default::default()).expect("Home route");
|
||||
fetch_user_info.dispatch(());
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=Page::Register.path()
|
||||
view=move |cx| {
|
||||
view! { cx, <Register api=unauthorized_api/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use leptos::*;
|
||||
|
||||
use client::*;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <App /> })
|
||||
mount_to_body(|cx| view! { cx, <App/> })
|
||||
}
|
||||
|
||||
@@ -6,15 +6,19 @@ use leptos_router::*;
|
||||
#[component]
|
||||
pub fn Home(cx: Scope, user_info: Signal<Option<UserInfo>>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<h2>"Leptos Login example"</h2>
|
||||
{move || match user_info.get() {
|
||||
Some(info) => view!{ cx,
|
||||
<p>"You are logged in with "{ info.email }"."</p>
|
||||
}.into_view(cx),
|
||||
None => view!{ cx,
|
||||
<p>"You are not logged in."</p>
|
||||
<A href=Page::Login.path() >"Login now."</A>
|
||||
}.into_view(cx)
|
||||
}}
|
||||
<h2>"Leptos Login example"</h2>
|
||||
{move || match user_info.get() {
|
||||
Some(info) => {
|
||||
view! { cx, <p>"You are logged in with " {info.email} "."</p> }
|
||||
.into_view(cx)
|
||||
}
|
||||
None => {
|
||||
view! { cx,
|
||||
<p>"You are not logged in."</p>
|
||||
<A href=Page::Login.path()>"Login now."</A>
|
||||
}
|
||||
.into_view(cx)
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use api_boundary::*;
|
||||
|
||||
use crate::{
|
||||
api::{self, AuthorizedApi, UnauthorizedApi},
|
||||
components::credentials::*,
|
||||
Page,
|
||||
};
|
||||
use api_boundary::*;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn Login<F>(cx: Scope, api: UnauthorizedApi, on_success: F) -> impl IntoView
|
||||
@@ -53,14 +51,14 @@ where
|
||||
let disabled = Signal::derive(cx, move || wait_for_response.get());
|
||||
|
||||
view! { cx,
|
||||
<CredentialsForm
|
||||
title = "Please login to your account"
|
||||
action_label = "Login"
|
||||
action = login_action
|
||||
error = login_error.into()
|
||||
disabled
|
||||
/>
|
||||
<p>"Don't have an account?"</p>
|
||||
<A href=Page::Register.path()>"Register"</A>
|
||||
<CredentialsForm
|
||||
title="Please login to your account"
|
||||
action_label="Login"
|
||||
action=login_action
|
||||
error=login_error.into()
|
||||
disabled
|
||||
/>
|
||||
<p>"Don't have an account?"</p>
|
||||
<A href=Page::Register.path()>"Register"</A>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use api_boundary::*;
|
||||
|
||||
use crate::{
|
||||
api::{self, UnauthorizedApi},
|
||||
components::credentials::*,
|
||||
Page,
|
||||
};
|
||||
use api_boundary::*;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
|
||||
@@ -52,26 +50,24 @@ pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
|
||||
let disabled = Signal::derive(cx, move || wait_for_response.get());
|
||||
|
||||
view! { cx,
|
||||
<Show
|
||||
when = move || register_response.get().is_some()
|
||||
fallback = move |_| view!{ cx,
|
||||
<CredentialsForm
|
||||
title = "Please enter the desired credentials"
|
||||
action_label = "Register"
|
||||
action = register_action
|
||||
error = register_error.into()
|
||||
disabled
|
||||
/>
|
||||
<p>"Your already have an account?"</p>
|
||||
<A href=Page::Login.path()>"Login"</A>
|
||||
}
|
||||
>
|
||||
<p>"You have successfully registered."</p>
|
||||
<p>
|
||||
"You can now "
|
||||
<A href=Page::Login.path()>"login"</A>
|
||||
" with your new account."
|
||||
</p>
|
||||
</Show>
|
||||
<Show
|
||||
when=move || register_response.get().is_some()
|
||||
fallback=move |_| {
|
||||
view! { cx,
|
||||
<CredentialsForm
|
||||
title="Please enter the desired credentials"
|
||||
action_label="Register"
|
||||
action=register_action
|
||||
error=register_error.into()
|
||||
disabled
|
||||
/>
|
||||
<p>"Your already have an account?"</p>
|
||||
<A href=Page::Login.path()>"Login"</A>
|
||||
}
|
||||
}
|
||||
>
|
||||
<p>"You have successfully registered."</p>
|
||||
<p>"You can now " <A href=Page::Login.path()>"login"</A> " with your new account."</p>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
|
||||
1
examples/login_with_token_csr_only/server/Makefile.toml
Normal file
1
examples/login_with_token_csr_only/server/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../../cargo-make/common.toml" }
|
||||
@@ -2,8 +2,7 @@ use crate::{application::*, Error};
|
||||
use api_boundary as json;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
response::{IntoResponse, Response},
|
||||
response::{IntoResponse, Json, Response},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{env, sync::Arc};
|
||||
|
||||
use api_boundary as json;
|
||||
use axum::{
|
||||
extract::{State, TypedHeader},
|
||||
headers::{authorization::Bearer, Authorization},
|
||||
@@ -8,10 +7,9 @@ use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::{env, sync::Arc};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
use api_boundary as json;
|
||||
|
||||
mod adapters;
|
||||
mod application;
|
||||
|
||||
@@ -25,7 +23,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
env::set_var("RUST_LOG", "debug");
|
||||
}
|
||||
env::VarError::NotUnicode(_) => {
|
||||
return Err(anyhow::anyhow!("The value of 'RUST_LOG' does not contain valid unicode data."));
|
||||
return Err(anyhow::anyhow!(
|
||||
"The value of 'RUST_LOG' does not contain valid unicode \
|
||||
data."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user