Compare commits

...

98 Commits

Author SHA1 Message Date
Greg Johnston
1657107834 missed some 2023-05-18 08:34:51 -04:00
Greg Johnston
a241e5edce tests: fix broken SSR doctests 2023-05-18 08:29:41 -04:00
Greg Johnston
85ad7b0f38 fix: <Suspense/> hydration when no resources are read under it (#1046) 2023-05-16 12:20:23 -04:00
Greg Johnston
f0a9940364 fix: leak in todomvc example (closes #706) 2023-05-15 14:53:39 -04:00
Mark Catley
b472aaf6a0 fix: typo in actix extract documentation (#1043) 2023-05-15 08:57:49 -04:00
Greg Johnston
059c1bf61c cargo fmt 2023-05-14 06:55:05 -04:00
Matt Crane
add13fd6a4 change: migrate Axum integration to use with_state over layer(Extension) (#1032) 2023-05-14 06:37:39 -04:00
Greg Johnston
904c2e8a67 v0.3.0 2023-05-13 19:44:06 -04:00
Greg Johnston
a5c3be586a docs: tweak new slice docs 2023-05-13 19:43:17 -04:00
Markus Kohlhase
9f5139d929 examples: fix trunk config to run tailwind at the right time (#1040) 2023-05-13 19:39:36 -04:00
sjud
bae305340e change: update create_slice to allow different types on getter and setter (#1036) 2023-05-13 19:39:17 -04:00
Greg Johnston
40c1556f29 change: remove APIs that had been marked deprecated (#1037) 2023-05-12 19:45:48 -04:00
Greg Johnston
0db4f5821f fix: avoid extra { escaping (closes #1035) (#1038) 2023-05-12 16:29:33 -04:00
Greg Johnston
12ebc95800 fix: flickering <Transition/> in release mode (closes #960) (#1030) 2023-05-11 14:51:33 -04:00
Greg Johnston
d7b919032e feat: SsrMode::PartiallyBlocked (#1026) 2023-05-10 13:30:01 -04:00
Greg Johnston
be8bf8b0d6 fix: corrects error-deserialization behavior of ActionForm (closes #1024) (#1025) 2023-05-09 06:40:22 -04:00
Greg Johnston
f84f1422f4 fix: maintain insertion order of meta tags (#1021) 2023-05-08 08:36:54 -04:00
Snêu
b01976e3bb examples: fix indentations (#1017) 2023-05-08 08:36:45 -04:00
agilarity
50b48fb272 chore: build CSS with trunk (#1016)
This configures a hook to run the tailwindcss CLI when a build is triggered or retriggered via Trunk watch. It eliminates the need to run the tailwindcss manually.
2023-05-08 08:36:07 -04:00
agilarity
1617e31d69 CI: clean up examples after verification (#1019)
* build: improve task names

* build: add clean-examples task

Make it easy to clean all the cargo and trunk files in the examples.

* build: clean after verify
2023-05-08 08:35:27 -04:00
Chris
51cd082d4c docs: add examples for manual server integration for router (#1015) 2023-05-08 08:34:43 -04:00
Warre Dujardin
72414b7945 docs: fix link to cargo-leptos README in the book (#1012) 2023-05-06 13:42:44 -04:00
FrankReh
1afa14ccbd docs: adjust Dynamic Attributes page (#1011)
Adjust the intro in the Dynamic Attributes page to include
the recent `dynamic style` feature. Also reorder a little given
the order of the page body that follows.
2023-05-06 13:42:16 -04:00
Warre Dujardin
477c29cdf1 docs: close iframe tag (#1013) 2023-05-06 13:41:50 -04:00
Greg Johnston
49a424314a docs: add a serevr fn section (#1014) 2023-05-06 13:14:16 -04:00
Warre Dujardin
598523cd9d fix: relax Debug trait bounds (#1010) 2023-05-06 12:10:48 -04:00
Greg Johnston
1fdb6f1cdf feat: add style: to view (#1009) 2023-05-06 06:23:20 -04:00
agilarity
9997487a9c test: lint examples with --all-features (#1008)
* test: lint all features

* fix(counter_isomorphic): check-style issues

* fix(errors_axum): check-style issues

* fix(hackernews): check-style issues

* fix(hackernews_axum): check-style issues

* fix(session_auth_axum): check-style issues

* build(session_auth_axum): add common tasks

* fix(ssr_modes): check-style issues

* build(ssr_modes_axum): add common tasks

* fix(ssr_modes_axum): check-style issues

* build(tailwind): add common tasks

* fix(tailwind): check-style issues

* fix(todo_app_sqlite_axum): check-style issues

* fix(todo_app_sqlite_viz): check-style issues
2023-05-05 22:25:29 -04:00
Greg Johnston
b5e94e4054 fix: properly dispose of <Suspense/> scopes (closes #834) (#1006) 2023-05-05 19:08:56 -04:00
Greg Johnston
a5f6e0bac4 docs: document that <ActionForm/> only works with form-encoded server functions (closes #977) (#1005) 2023-05-05 13:37:53 -04:00
Douglas Parsons
2c9de79576 docs: Reduce firmness of overlapping signals warnings (#1004)
Following [discord
question](https://discord.com/channels/1031524867910148188/1049869221636620300/1104043773194928163)
2023-05-05 11:28:36 -04:00
agilarity
63dd00a050 fix: lint issues in todomvc example (#1001)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:28:14 -04:00
agilarity
99823a3d4f fix: lint issues in todo_app_sqlite_viz example (#1000)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:38 -04:00
agilarity
c0bdd464f6 fix: lint issues in todo_app_sqlite_axum example (#999)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:27 -04:00
agilarity
7e7377f4f7 fix: lint issues in todo_app_sqlite example (#998)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:11 -04:00
agilarity
15448765dd fix: lint issues in session_auth_axum example (#997)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:26:44 -04:00
agilarity
f0f1c3144b fix: lint issues in router example (#996)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:26:24 -04:00
agilarity
630da4212d fix: lint issues in login_with_token_csr_only example (#995)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:26:09 -04:00
agilarity
38bc24bb9e fix: lint issues in hackernews_axum example (#992)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:25:24 -04:00
agilarity
012285337b fix: lint issues in hackernews example (#991)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:25:13 -04:00
agilarity
3ba4f62cef fix: lint issues in fetch example (#989)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:24:28 -04:00
agilarity
b4996769c1 fix: lint issues in errors_axum example (#988) 2023-05-05 11:23:59 -04:00
Greg Johnston
9a6b1f53da fix: lint issues in counters example
fix: lint issues in `counters` example
2023-05-05 11:23:38 -04:00
Greg Johnston
ef45828ca7 fix: don't assume OutOfOrder and GET for / 2023-05-05 10:20:36 -04:00
Greg Johnston
ea153e4f26 docs: error when component ends with view! { ... }; (closes #985) (#993) 2023-05-03 18:15:02 -04:00
Greg Johnston
59b8626277 docs: switch from compile errors to runtime warnings for incompatible feature flags (#990) 2023-05-03 16:25:35 -04:00
Greg Johnston
d8e03773f0 feat: allow structs in server function arguments (#987) 2023-05-03 15:26:48 -04:00
Joseph Cruz
5ab799bbf8 test: resolve check-style issues 2023-05-03 12:34:03 -04:00
Greg Johnston
6c763a83cb fix: suppress warning loading local resource without <Suspense/> in hydrate mode (closes #979) (#984) 2023-05-03 11:22:34 -04:00
agilarity
9cf337309d Fix lint issues in counter_isomorphic example (#980) 2023-05-02 20:22:07 -04:00
Greg Johnston
1af35cdd3b feat: add builder syntax for optional event listener (#969) 2023-05-02 15:47:19 -04:00
agilarity
fcb98474b8 examples: fix the lint issues in the counter example (#971) 2023-05-01 17:27:29 -04:00
Greg Johnston
54f7e9366a change/fix: require FromStr errors on Params to be Send + Sync so they are ErrorBoundary compatible (#974) 2023-05-01 17:18:46 -04:00
Matt Crane
ddf9df2b5e change: replace serde_urlencoded with serde_html_form to support Vec<_> in server fn args (#973) 2023-05-01 17:17:45 -04:00
Greg Johnston
7fe9f82d89 v0.3.0-alpha (#968) 2023-04-28 19:30:16 -04:00
Roland Fredenhagen
661adc4027 feat: ```view code block in doc comments for properties (#961) 2023-04-28 16:03:04 -04:00
Roland Fredenhagen
1011c464dc feat: add collect_view(cx) (#956) 2023-04-28 16:02:24 -04:00
Frank Panetta
4b498a3b42 chore: fix typos (#964) 2023-04-28 12:10:48 -04:00
yuuma03
3c90b47e77 fix: allow mounting multiple Leptos apps on same server (#966)
Use a HashMap indexed by base URL to cache route branches on the server.
2023-04-28 12:10:02 -04:00
Greg Johnston
671b1e4a8f docs: note need for serde dependency for server functions (closes #947) (#962) 2023-04-27 17:15:29 -04:00
agilarity
52021be806 tests: add wasm web test and common tasks (#954)
* test: rename web test module

* test: extract wasm-web-test task

* test: introduce common tasks

* test: add web-test and common tasks
2023-04-27 17:00:13 -04:00
Roland Fredenhagen
75a7bd610a fix: escapes in doc comments on component properties (#958) 2023-04-27 16:43:38 -04:00
Greg Johnston
de553cf4fe docs: add note on projecting children (#959) 2023-04-26 20:08:12 -04:00
Greg Johnston
0a65f43789 fix: <ErrorBoundary/> toggling between states (closes issue #820) (#957) 2023-04-26 17:30:30 -04:00
Greg Johnston
0f277c55ec fix: use absolute reference to ::leptos::Scope in case not imported 2023-04-25 16:52:14 -04:00
Greg Johnston
04b01a6ced docs: add note about adding CSS classes 2023-04-25 16:26:08 -04:00
Ben Wishovich
6c3381ce52 feat: add From for RequestParts into Parts for Axum and add an option to ge… (#931) 2023-04-24 20:08:28 -04:00
Roland Fredenhagen
fa2e2248d3 feat: impl FromIterator for View (#945) 2023-04-24 20:07:27 -04:00
jquesada2016
362150a715 feat: implemented IntoView for component props (#948) 2023-04-24 20:05:31 -04:00
agilarity
27b5991ee3 examples: test business logic for counter_without_macros (#927) 2023-04-24 20:04:40 -04:00
Greg Johnston
0a7dbb0ca4 feat: add Actix extract helper (#936) 2023-04-24 20:03:24 -04:00
yuuma03
234861a156 fix: generics on impl From slot to Vec<slot> (#946) 2023-04-24 20:03:03 -04:00
Greg Johnston
78d6d312f8 CI: fix unused variables breaking tests (#950) 2023-04-24 17:19:10 -04:00
Greg Johnston
a1144a5b6b examples: add autofocus in todomvc 2023-04-24 08:07:49 -04:00
Greg Johnston
9723cc466e fix: rust-analyzer/cargo fmt issues with LEPTOS_OUTPUT_NAME 2023-04-24 08:00:36 -04:00
Greg Johnston
79c12c0129 examples: better practice for view types in todos (#940) 2023-04-23 17:33:45 -04:00
Jonas Matser
a08d6bae10 fix: ServerFnError::Serialization error string (#939) 2023-04-23 16:17:20 -04:00
Daniel Santana
39261a276c docs: add the GetCbor and GetJson to server macro documentation. (#938) 2023-04-23 15:42:05 -04:00
Roland Fredenhagen
c471986024 feat: add #[allow(missing_docs)] to children prop in components (#934) 2023-04-23 15:34:42 -04:00
Roland Fredenhagen
d2e3a156e8 fix: link to actual type instead of Into trait for component properties (#932) 2023-04-23 15:33:27 -04:00
Fabian Keller
9badfa997b examples: add timer example with reactive use_interval hook (#925) 2023-04-23 15:27:47 -04:00
Ben Wishovich
72f8bf4e20 feat: remove need for LEPTOS_OUTPUT_NAME env var after compilation (#899) 2023-04-23 15:20:47 -04:00
Greg Johnston
c74b15b120 docs: add section on WASM binary size 2023-04-23 15:07:48 -04:00
Craig Rodrigues
9a4f3ab08c chore: specify dependency version for cached (#929) 2023-04-22 17:51:40 -04:00
Greg Johnston
a0935c169e docs: add some content on server-side rendering (#930) 2023-04-22 15:15:48 -04:00
yuuma03
0e2181fb90 fix: allow nested slots (#928) 2023-04-22 14:14:01 -04:00
Greg Johnston
732ec14302 docs: add use of batch to avoid BorrowMut panic 2023-04-22 07:03:10 -04:00
agilarity
ec95060b6e fix: features related compile error (#919)
`cargo make test` sets the --all-features flag by default. This change
clears it.
2023-04-22 06:50:35 -04:00
J
689afec26e docs: fixed typo in interlude_styling.md (#924) 2023-04-22 06:49:15 -04:00
J
bbf23ea40a docs: removed extra unused code blocks in form.md (#923) 2023-04-22 06:48:28 -04:00
J
34e0a8e47d docs: fixed a minor typo in async readme (#921) 2023-04-22 06:47:44 -04:00
Ben Wishovich
81f330e888 feat: add thorough tracing throughout (#908) 2023-04-22 06:47:11 -04:00
Greg Johnston
e5d657dd55 fix: panic when creating nested StoredValue (#920) 2023-04-22 06:44:25 -04:00
Greg Johnston
f919127a7e fix some issues with animated routing (#889) 2023-04-21 15:33:14 -04:00
Greg Johnston
2001bd808f examples: fix broken counters tests (#915) 2023-04-21 15:26:18 -04:00
yuuma03
f51857cedc feat: add slots (closes #769) (#909) 2023-04-21 14:36:38 -04:00
Greg Johnston
f3b8d27c4f change: add window_event_listener_untyped and deprecate window_event_listener pending 0.3.0 (#913) 2023-04-21 14:14:35 -04:00
Greg Johnston
d3a577c365 cargo fmt 2023-04-21 12:45:08 -04:00
216 changed files with 5297 additions and 1937 deletions

2
.gitignore vendored
View File

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

View File

@@ -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

View File

@@ -49,6 +49,7 @@ dependencies = [
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
{ name = "check", path = "examples/session_auth_axum" },
{ name = "check", path = "examples/slots" },
{ name = "check", path = "examples/ssr_modes" },
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
@@ -68,13 +69,31 @@ 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"
@@ -87,8 +106,15 @@ 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 = ""
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"

View File

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

View File

@@ -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>

View File

@@ -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)
@@ -30,20 +31,22 @@
- [Interlude: Styling](./interlude_styling.md)
- [Metadata]()
- [Server Side Rendering](./ssr/README.md)
- [`cargo-leptos`]()
- [The Life of a Page Load](./ssr/21_life_cycle.md)
- [Async Rendering and SSR “Modes”](./ssr/22_ssr_modes.md)
- [Hydration Footguns]()
- [Request/Response]()
- [`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)

View File

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

View File

@@ -1,9 +1,9 @@
# Working with `async`
So far weve only been working with synchronous users interfaces: You provide some input,
the app immediately process it and updates the interface. This is great, but is a tiny
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 theres much more to it than that.
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
In this chapter, well see how Leptos helps smooth out that process for you.

View 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 were 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 wont 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. Heres 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 cant 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 dont 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, its 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
```
Its captured through multiple levels of children that need to run more than once, and theres 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.

View File

@@ -109,4 +109,4 @@ pub fn MyComponent(cx: Scope) -> impl IntoView {
## Contributions Welcome
Leptos no opinions on how you style your website or app, but were very happy to provide support to any tools youre trying to create to make it easier. If youre working on a CSS or styling approach that youd like to add to this list, please let us know!
Leptos has no opinions on how you style your website or app, but were very happy to provide support to any tools youre trying to create to make it easier. If youre working on a CSS or styling approach that youd like to add to this list, please let us know!

View File

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

View File

@@ -0,0 +1,125 @@
# Server Functions
If youre creating anything beyond a toy app, youll 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 dont 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 thats stored on the server and definitely shouldnt be shipped down to a users 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 users dark/light mode preference across sessions, and be applied during server rendering so theres 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, youll 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 functions 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? Its 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();
// ...
}
```
Youll 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 doesnt have access to client state unless you send it explicitly. (Otherwise wed 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 dont 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 its 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 well 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).)
- Youll 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 well 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 its very important to remember. **Server functions are not magic; theyre syntax sugar for defining a public API.** The _body_ of a server function is never made public; its just part of your server binary. But the server function is a publicly accessible API endpoint, and its 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 Ive 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, well 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.

View 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 _cant,_ like reading from and writing to a SQL database.
If youre used to building JavaScript frontend apps, youre probably used to calling out to some kind of REST API to do this sort of server work. If youre 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 its 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.

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
# `view`: Dynamic Attributes and Classes
# `view`: Dynamic Classes, Styles and Attributes
So far weve 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, well look at how to update attributes and classes dynamically,
In this section, well look at how to update classes, styles and attributes dynamically,
and well introduce the concept of a **derived signal**.
Lets 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 cant 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 youre 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

View File

@@ -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 doesnt 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,13 +1,13 @@
extend = [{ path = "./cargo-make/common.toml" }]
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
# Emulate workspace
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
#"counters", - FIXME: test compile errors
"counters",
"counters_stable",
"counter_without_macros",
"error_boundary",
@@ -31,7 +31,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
dependencies = ["pre-verify", "verify", "post-verify"]
[tasks.verify]
description = "Run all quality checks and tests"
@@ -41,20 +41,17 @@ dependencies = ["check-style", "test-unit-and-web"]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.pre-verify]
[tasks.pre-verify-flow]
[tasks.post-verify-flow]
[tasks.post-verify]
dependencies = ["clean-all"]
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
dependencies = ["pre-web-test", "web-test", "post-web-test"]
[tasks.pre-web-test-flow]
[tasks.pre-web-test]
[tasks.web-test]
[tasks.post-web-test-flow]
[tasks.post-web-test]

View 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"]

View File

@@ -0,0 +1,4 @@
[tasks.web-test]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]

View File

@@ -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"]

View File

@@ -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);

View File

@@ -4,10 +4,12 @@ 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
/>
}
})
}

View File

@@ -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()

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -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>

View File

@@ -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::*;

View File

@@ -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)?

View File

@@ -17,6 +17,7 @@ console_error_panic_hook = "0.1.7"
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"]

View File

@@ -1,9 +1,7 @@
[env]
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
[tasks.web-test]
command = "cargo"
args = ["make", "wasm-pack-test"]
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
]
[tasks.build]
command = "cargo"

View File

@@ -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;
}
}

View 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);
}
}

View File

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

View File

@@ -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"]

View File

@@ -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>
}
}

View File

@@ -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()
);
});
}

View 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>"
);
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -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();

View File

@@ -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>

View File

@@ -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::*;

View File

@@ -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`

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -14,7 +14,7 @@ pub enum FetchError {
#[error("Error loading data from serving.")]
Request,
#[error("Error deserializaing cat data from request.")]
Json
Json,
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {
@@ -55,7 +55,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
.collect::<Vec<_>>()
.collect_view(cx)
})
};
@@ -76,7 +76,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
data.map(|data| {
data.iter()
.map(|s| view! { cx, <span>{s}</span> })
.collect::<Vec<_>>()
.collect_view(cx)
})
})
};

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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,

View File

@@ -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,
<>

View File

@@ -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>

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -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,
{

View File

@@ -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

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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`

View File

@@ -1,4 +1,4 @@
use leptos::{component, Scope, IntoView, view};
use leptos::{component, view, IntoView, Scope};
use leptos_router::*;
#[component]

View File

@@ -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,

View File

@@ -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,
<>

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -0,0 +1 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -0,0 +1 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -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,

View File

@@ -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>
}
}

View File

@@ -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>
}
}

View File

@@ -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>
}
}

View File

@@ -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/> })
}

View File

@@ -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)
}
}}
}
}

View File

@@ -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>
}
}

View File

@@ -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>
}
}

View File

@@ -0,0 +1 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -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;

View File

@@ -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."
));
}
}
}

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -97,7 +97,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
<li><A href=contact.id.to_string()><span>{&contact.first_name} " " {&contact.last_name}</span></A></li>
}
})
.collect::<Vec<_>>()
.collect_view(cx)
})
};
@@ -107,7 +107,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
{move || view! { cx, <ul>{contacts}</ul>}}
</Suspense>
<AnimatedOutlet
<AnimatedOutlet
class="outlet"
outro="fadeOut"
intro="fadeIn"

View File

@@ -41,11 +41,11 @@ a[aria-current] {
}
.slideIn {
animation: 0.125s slideIn forwards;
animation: 0.25s slideIn forwards;
}
.slideOut {
animation: 0.125s slideOut forwards;
animation: 0.25s slideOut forwards;
}
@keyframes slideIn {
@@ -67,11 +67,11 @@ a[aria-current] {
}
.slideInBack {
animation: 0.125s slideInBack forwards;
animation: 0.25s slideInBack forwards;
}
.slideOutBack {
animation: 0.125s slideOutBack forwards;
animation: 0.25s slideOutBack forwards;
}
@keyframes slideInBack {

View File

@@ -1,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,9 +1,7 @@
use std::collections::HashSet;
use cfg_if::cfg_if;
use leptos::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
cfg_if! {
if #[cfg(feature = "ssr")] {
@@ -168,7 +166,9 @@ pub async fn login(
.ok_or("User does not exist.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
match verify(password, &user.password).map_err(|e| ServerFnError::ServerError(e.to_string()))? {
match verify(password, &user.password)
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
true => {
auth.login_user(user.id);
auth.remember_user(remember.is_some());

View File

@@ -13,7 +13,9 @@ impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
TodoAppError::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}

View File

@@ -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},
};
@@ -13,16 +13,16 @@ if #[cfg(feature = "ssr")] {
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
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 mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);

View File

@@ -5,8 +5,8 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::{post, get},
extract::{Path, Extension, RawQuery},
routing::get,
extract::{Path, State, Extension, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
Router,
@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
use session_auth_axum::*;
use session_auth_axum::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
use leptos::{log, view, provide_context, LeptosOptions, get_configuration, ServerFnError};
use leptos::{log, view, provide_context, LeptosOptions, get_configuration};
use std::sync::Arc;
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
@@ -33,7 +33,7 @@ if #[cfg(feature = "ssr")] {
}, request).await
}
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
move |cx| {
provide_context(cx, auth_session.clone());
@@ -68,7 +68,7 @@ if #[cfg(feature = "ssr")] {
// 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, <TodoApp/> }).await;
@@ -80,8 +80,8 @@ if #[cfg(feature = "ssr")] {
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
.with_config(auth_config))
.layer(SessionLayer::new(session_store))
.layer(Extension(Arc::new(leptos_options)))
.layer(Extension(pool));
.layer(Extension(pool))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View File

@@ -1,5 +1,4 @@
use crate::auth::*;
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::{auth::*, error_template::ErrorTemplate};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
@@ -21,15 +20,15 @@ if #[cfg(feature = "ssr")] {
use sqlx::SqlitePool;
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
Ok(use_context::<SqlitePool>(cx)
use_context::<SqlitePool>(cx)
.ok_or("Pool missing.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
pub fn auth(cx: Scope) -> Result<AuthSession, ServerFnError> {
Ok(use_context::<AuthSession>(cx)
use_context::<AuthSession>(cx)
.ok_or("Auth session missing.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
pub fn register_server_functions() {
@@ -73,7 +72,8 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let pool = pool(cx)?;
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
let mut rows =
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
while let Some(row) = rows
.try_next()
@@ -111,11 +111,13 @@ pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
match sqlx::query("INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)")
.bind(title)
.bind(id)
.execute(&pool)
.await
match sqlx::query(
"INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)",
)
.bind(title)
.bind(id)
.execute(&pool)
.await
{
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
@@ -241,11 +243,11 @@ pub fn Todos(cx: Scope) -> impl IntoView {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
} else {
todos
.into_iter()
@@ -266,9 +268,8 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</ActionForm>
</li>
}
.into_any()
})
.collect::<Vec<_>>()
.collect_view(cx)
}
}
})
@@ -287,7 +288,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect::<Vec<_>>()
.collect_view(cx)
};
view! {
@@ -305,7 +306,10 @@ pub fn Todos(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Login(cx: Scope, action: Action<Login, Result<(), ServerFnError>>) -> impl IntoView {
pub fn Login(
cx: Scope,
action: Action<Login, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
cx,
<ActionForm action=action>
@@ -331,7 +335,10 @@ pub fn Login(cx: Scope, action: Action<Login, Result<(), ServerFnError>>) -> imp
}
#[component]
pub fn Signup(cx: Scope, action: Action<Signup, Result<(), ServerFnError>>) -> impl IntoView {
pub fn Signup(
cx: Scope,
action: Action<Signup, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
cx,
<ActionForm action=action>
@@ -363,7 +370,10 @@ pub fn Signup(cx: Scope, action: Action<Signup, Result<(), ServerFnError>>) -> i
}
#[component]
pub fn Logout(cx: Scope, action: Action<Logout, Result<(), ServerFnError>>) -> impl IntoView {
pub fn Logout(
cx: Scope,
action: Action<Logout, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
cx,
<div id="loginbox">

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

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

View File

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

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

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

View File

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

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -44,7 +44,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a></li>})
.collect::<Vec<_>>()
.collect_view(cx)
})
)
};
@@ -109,7 +109,7 @@ fn Post(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
.collect::<Vec<_>>()
.collect_view(cx)
}
</ul>
</div>

View File

@@ -12,8 +12,8 @@ async fn main() -> std::io::Result<()> {
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
GetPost::register();
ListPostMetadata::register();
let _ = GetPost::register();
let _ = ListPostMetadata::register();
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;

View File

@@ -1,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -49,7 +49,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a> "|" <a href=format!("/post_in_order/{}", post.id)>{&post.title}"(in order)"</a></li>})
.collect::<Vec<_>>()
.collect_view(cx)
})
)
};
@@ -114,7 +114,7 @@ fn Post(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
.collect::<Vec<_>>()
.collect_view(cx)
}
</ul>
</div>

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