Compare commits

...

206 Commits

Author SHA1 Message Date
Greg Johnston
8943539043 docs: clarify WASM target 2023-07-09 17:05:37 -04:00
Joseph Cruz
f6a272498d test(counters_stable/wasm): enter count (#1307) 2023-07-08 12:07:11 -04:00
Ari Seyhun
aef7c4ce8e perf: use lazy thread local for regex in router match_optionals (#1309) 2023-07-08 08:47:52 -04:00
Greg Johnston
b29eb8e032 fix: <ActionForm/> should check origin correctly before doing a full-page refresh (#1304) 2023-07-08 08:00:48 -04:00
Greg Johnston
da9183f4b5 docs: fix braces in <Show/> example (#1303) 2023-07-08 06:42:27 -04:00
Greg Johnston
ae3ddcb0e6 docs: must use View (#1302) 2023-07-08 06:42:02 -04:00
Greg Johnston
c6b8f0e8ed v0.4.2 2023-07-07 15:34:56 -04:00
g-re-g
bab9f40a81 fix: rework diff functionality for <For/> (#1296) 2023-07-07 15:32:26 -04:00
webmstk
c2cfdf3678 docs: fixed typo in parent-child doc (#1300) 2023-07-07 13:59:08 -04:00
Joseph Cruz
8967eadc02 test(counters_stable): add wasm testing (#1278)
* test(counters_stable/wasm): view counters > counts

* test(counters_stable/wasm): view counters > title

* test(counters_stable/wasm): add counter

* test(counters_stable/wasm): add 1k counters

* clear(counters_stable/wasm): clear counters

* test(counters_stable/wasm): increment counters

* test(counters_stable/wasm): decrement counter

* test(counters_stable/wasm): remove counter
2023-07-07 13:07:27 -04:00
Greg Johnston
4cc65f837f chore: add mdbook in flake (#1299)
* nix-flake: use follows for `rust-overlay` (..)

This removes the extra nixpkgs dependency by re-using the nixpkgs
already defined.

https://nixos.wiki/wiki/Flakes
https://web.archive.org/web/20230621091703/https://nixos.wiki/wiki/Flakes

> # The `follows` keyword in inputs is used for inheritance.
> # Here, `inputs.nixpkgs` of sops-nix is kept consistent with the `inputs.nixpkgs` of
> # the current flake, to avoid problems caused by different versions of nixpkgs.

* nix-flake: use nix overlays for rustc/cargo (..)

This allows the same nightly rust version to be used across the
flake (vs. strictly in the `devShell`).

* nix-flake: add `mdbook` to the development shell (..)

bumped `nixpkgs` to the latest unstable to pull in mdbook version `0.4.30`.

* nix-flake: expose all rust tools in dev environment (..)

The `oxalica / rust-overlay` docs expose all tools in the development
environment, so we should do the same:

https://github.com/oxalica/rust-overlay#use-in-devshell-for-nix-develop

---------

Co-authored-by: Jay Querie <jay@querie.cc>
2023-07-07 12:59:18 -04:00
Dessalines
22706e7371 docs: adding instructions to add a tailwind plugin to examples. (#1293) 2023-07-07 12:35:24 -04:00
webmstk
9f9302662c docs/examples: make error handling example more obvious for Chrome users (#1292) 2023-07-07 12:34:57 -04:00
Greg Johnston
6b90e1babd examples: add 404 support in Actix examples (closes #1031) (#1291) 2023-07-06 10:35:37 -04:00
sjud
7e540a8f49 feat: support Axum extractors with state other than () (#1275)
This requires state to be provided via context using a special handler, but allows for extractors that use this state, rather than only `()`, as previously.
2023-07-05 20:40:29 -04:00
Greg Johnston
f06ffd72aa fix: use once_cell::OnceCell rather than std::OnceCell (#1288) 2023-07-05 17:16:51 -04:00
Greg Johnston
83d3d7579c fix: issue with class hydration not removing classes correctly (closes #1286) (#1287) 2023-07-05 12:00:27 -04:00
Greg Johnston
39edb6eb45 fix: untracked read in <Redirect/> (#1280) 2023-07-04 11:52:13 -04:00
Greg Johnston
d81c1a929e fix: duplicate text nodes during <For/> hydration (closes #1279) (#1281) 2023-07-04 11:50:59 -04:00
Greg Johnston
f69c28df18 fix: improved diagnostics about non-reactive signal access (#1277) 2023-07-03 19:37:15 -04:00
Greg Johnston
66f54e7f1a docs: add docs on responses/redirects and clarification re: Axum State(_) extractors (#1272) 2023-07-03 09:58:02 -04:00
Greg Johnston
81e416b085 fix: error messages in dyn_classes (#1270) 2023-07-03 09:57:50 -04:00
Marc-Stefan Cassola
a5f73b441c feat: added watch helper (#1262) 2023-07-03 09:29:40 -04:00
Greg Johnston
0f1ebccad5 fix: clearing <For/> that has a previous sibling in release mode (fixes #1258) (#1267) 2023-07-02 17:27:39 -04:00
Greg Johnston
2f01df6185 fix: HtmlElement::dyn_classes() when adding classes (#1265) 2023-07-02 17:27:24 -04:00
martin frances
c4982319fe chore: ran cargo clippy --fix and reviewed changes. (#1259) 2023-07-02 17:27:14 -04:00
Michael Zimmermann
8fb4e88439 feat: implement PartialEq on ServerFnError (#1260)
This allows returning it in a memo.
2023-07-02 17:22:06 -04:00
Greg Johnston
e821efca07 chore: new cargo fmt (#1266) 2023-07-02 17:01:39 -04:00
Sridhar Ratnakumar
568f7b21ae example/readme: Link to 'VS Browser' ext; format. (#1261) 2023-07-02 16:56:59 -04:00
Greg Johnston
d3c0f5320c docs: update 02_getting_started.md (#1256) 2023-06-30 17:26:33 -04:00
Greg Johnston
5adc88bf50 fix: hot-reloading view marker line number (#1255) 2023-06-30 14:03:54 -04:00
Greg Johnston
67300adf41 fix: regression in ability to use signals directly in the view in stable (#1254) 2023-06-30 11:59:29 -04:00
afiqzx
4a3a67bf37 feat: add fallback support for workspace in get_config_from_str (#1249) 2023-06-30 10:44:27 -04:00
Dương
8150847218 test(router_example): add playwright tests (#1247)
* implemented e2e tests for router example

* chore(router_example): cleanup e2e/package.json
2023-06-30 10:40:33 -04:00
Greg Johnston
8cb95b4646 docs: update server fn docs (#1252) 2023-06-30 10:40:06 -04:00
Joseph Cruz
df4ce904a0 test(counters_stable): add missing e2e tests (#1251)
* test(counters_stable): remove unused ids

* test(counters_stable): enter count

* refactor(counters_stable/e2e): improve test names

* refactor(counters_stable): move page object

* refactor(counters_stable/e2e): target nth counter

* test(counters_stable): remove counter

* refactor(counters_stable/e2e): change description
2023-06-30 08:52:48 -04:00
Ari Seyhun
1cc3a43268 chore: remove unused variable warnings with ssr props (#1244) 2023-06-30 08:05:20 -04:00
Greg Johnston
d5a862a406 v0.4.0 (#1250) 2023-06-30 07:51:07 -04:00
Joseph Cruz
33c83c3e62 fix(counters_stable): intermittent closed target errors (#1240) (#1243) 2023-06-29 07:19:42 -04:00
Greg Johnston
171adcd09e docs: update README re: nightly on 0.3 2023-06-27 15:48:04 -04:00
Greg Johnston
13f7cb9a9a fix: add missing attribute-escaping logic in leptos_meta and class attributes in SSR (closes #1238) (#1241) 2023-06-27 11:53:23 -04:00
Greg Johnston
ee7dbafc85 change: migrate to nightly and csr features rather than stable and default-features = false (#1227) 2023-06-26 21:12:14 -04:00
Joseph Cruz
f5cfe4e8a2 test(counters_stable): add playwright tests (#1235) 2023-06-26 21:11:09 -04:00
Greg Johnston
c3e45d19d7 docs: typo (#1237) 2023-06-26 10:05:16 -04:00
Greg Johnston
966100c2d6 feat: add an anyhow-like Result type for easier error handling (#1228) 2023-06-25 15:18:00 -04:00
Greg Johnston
bce1dea11b fix: make <Transition/> transparent like <Suspense/> to avoid Scope issues (closes #1231) (#1232) 2023-06-24 17:07:07 -04:00
Greg Johnston
c55067ab7c feat: improved error handling and version tracking for pending actions/<ActionForm/> (closes #1205) (#1225) 2023-06-23 11:10:59 -04:00
Greg Johnston
9da4084561 fix: nested Suspense/Transition with cascading resources (#1214) 2023-06-21 16:39:58 -04:00
jquesada2016
1d7235d4ca fix: in <For/>, removed not being cleared when setting a diff to clear, causing panic (#1220) 2023-06-21 15:19:12 -04:00
Greg Johnston
2cb8171105 docs: document <ErrorBoundary/>/<Suspense/> relationship (#1210) 2023-06-21 11:17:20 -04:00
Lukas Potthast
bbc7799b7c fix: memo with_untracked (#1213) 2023-06-21 10:13:18 -04:00
Joseph Cruz
a9cbcce8b2 fix(examples/tailwind): host system is missing dependencies to run browsers (#1216) 2023-06-21 08:07:20 -04:00
Greg Johnston
3531ca64bb examples: update leptos-tailwind-axum to use main branch (#1218) 2023-06-21 08:06:52 -04:00
Ty Larrabee
e402b85dd6 docs: a few grammar fixes, removal of ref to ComponentProps (#1217) 2023-06-20 21:00:28 -04:00
Greg Johnston
8ae5cf0ccf fix: don't re-mount identical child in DynChild (#1211) 2023-06-19 17:04:08 -04:00
Tristan Guichaoua
5c34c3fc77 docs: fix typo in "working with signals" (#1208) 2023-06-19 10:36:18 -04:00
Greg Johnston
3a570dc0d9 docs: this was causing a Google search indexing issue... 2023-06-18 09:10:01 -04:00
Joseph Cruz
3c6748b30d build(examples): generate workspace members variable (#1201)
* build(examples): add gen members task

* build(examples): include members variable
2023-06-17 18:19:01 -04:00
Cherry
24945f67bf docs: add note to docs on how to fix failing builds which rebuild-std (#1200)
Fixes #1199
2023-06-17 16:51:57 -04:00
Joseph Cruz
edddab1e51 ci(examples): automatically keep the list of example projects current (#1198) 2023-06-17 16:51:31 -04:00
Greg Johnston
acfc86d2a4 fix: SVG <use> in SSR (#1203) 2023-06-17 16:47:39 -04:00
Greg Johnston
651868dec9 fix: animations on multiple back navigations (closes #1088) (#1204) 2023-06-17 16:47:19 -04:00
Joseph Cruz
18bc03e660 ci(examples): split check example and improve workflows (#1191) 2023-06-15 21:44:37 -04:00
jquesada2016
5f0013e482 fix: reorder <For /> apply_diff steps (#1196) 2023-06-15 20:37:17 -04:00
martin frances
10c0a2de65 chore: cleared clippy warnings (#1190)
The change in indentation makes the PR hard to review

so I will discuss the change in conversational language

Two "if"'s checks were merged into one "if"

this

-        if let Some(expr) = node.value() {
-            if let syn::Expr::Tuple(tuple) = expr {

becomes

+        if let Some(Tuple(tuple)) = node.value() {
2023-06-15 20:11:50 -04:00
Karim Lalani
b24be2566d docs: renamed function names in 13-actions chapter of book to reduce confusion (#1175) 2023-06-15 20:09:46 -04:00
Greg Johnston
77439b5db5 fix: setting set_pending now that <Transition/> body doesn't re-render (#1193) 2023-06-15 20:09:22 -04:00
Greg Johnston
23594a43ea fix: allow FnOnce extractors (#1192) 2023-06-15 20:09:13 -04:00
hchockarprasad
601db7aa86 fix: handle nested data in serde_qs deserialization correctly (#1183) 2023-06-15 10:15:10 -04:00
Joseph Cruz
d15ba11104 fix(examples/js-framework-benchmark): error: cannot find macro template in this scope (#1182) (#1189) 2023-06-15 08:19:38 -04:00
Joseph Cruz
d45d92433f ci(examples): include all example projects (#1188) 2023-06-14 15:16:14 -04:00
jquesada2016
97127a90c6 fix: new <For/> bug when clearing which ignores further additions (#1181) 2023-06-14 13:56:56 -04:00
martin frances
55bb63edea chore: updated cached 0.43.0 to 0.44.0 (#1187) 2023-06-14 11:07:24 -04:00
martin frances
15a4e54435 chore: criterion was outdated version 0.4.0 becomes 0.5.1. (#1184) 2023-06-14 11:06:50 -04:00
Joseph Cruz
3a522aef5d ci(examples): split jobs and verify changed examples (#1155) 2023-06-13 21:29:54 -04:00
Greg Johnston
a98885a123 fix: <ErrorBoundary/> IDs with new hydration key system (#1180) 2023-06-13 18:38:23 -04:00
Greg Johnston
2b7923261b docs: fix failing doctests from server fn docs (#1179) 2023-06-13 17:49:16 -04:00
Greg Johnston
b043f829a6 docs: clarify available server fn encodings (#1178) 2023-06-13 16:01:45 -04:00
jquesada2016
f415f7b146 fix: removes in new <For/> causing panics in some circumstances (#1173) 2023-06-13 15:43:02 -04:00
martin frances
4e4e6864dd chore: clear virtual-workspace resolver warnings since Rust 1.70 (#1174)
This patch just clears the warnings listed below and ensures we get the benefits of a better package manger function.

```console
warning: some crates are on edition 2021 which defaults to `resolver = "2"`, but virtual workspaces default to `resolver = "1"`
note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
note: to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
```console
2023-06-13 14:54:39 -04:00
Nova
b0a23be07b fix: replace ouroboros with self_cell (#1171) 2023-06-12 07:27:52 -04:00
devriesp
f602cd7b5e docs: typos (#1172) 2023-06-12 07:26:54 -04:00
martin frances
6fac92cb62 perf: removed duplicate calls to .collect() and .into_iter() in leptos_actix (#1133) 2023-06-11 21:54:24 -04:00
jquesada2016
b6d9060152 feat: improved <For/> algorithm (#1146)
Rewrites the algorithm behind the `<For/>` component to create a more robust keyed list implementation, with the potential for future additional optimizations related to grouping moved ranges.

Closes #533.
2023-06-11 10:20:14 -04:00
Greg Johnston
bb10b32200 feat: register server functions automatically (#1154) 2023-06-11 09:09:21 -04:00
funlennysub
e0be2fa4ba feat: add additional support for generics in components (closes #949 + #1023) (#1109) 2023-06-10 16:44:27 -04:00
Greg Johnston
97d2829941 feat: clone <Suspense/> children instead of calling again (closes #398) (#1157) 2023-06-10 15:48:40 -04:00
Paul Wagener
5779242bd7 feat: add versioning to Resource to ensure it only returns latest refetch (closes #1124) (#1165) 2023-06-10 13:33:07 -04:00
Greg Johnston
abf90358fa fix: pass through docs for server functions (#1164) 2023-06-09 16:07:08 -04:00
Greg Johnston
76b73acb30 feat: enable bind: syntax for <Await/> component (#1158) 2023-06-09 09:08:26 -04:00
Greg Johnston
b24910271a fix: external redirects in <ActionForm/> (#1160) 2023-06-09 09:08:04 -04:00
Greg Johnston
3d75c71bfa build: better GitHub Action names 2023-06-06 17:04:17 -04:00
Greg Johnston
8096d7c416 docs: add sections on progressive enhancement/graceful degradation and <ActionForm/> (#1151) 2023-06-05 21:20:42 -04:00
Greg Johnston
17adf7cc14 feat: pass components with no props directly into the view as a function that takes only Scope (#1144) 2023-06-05 20:48:22 -04:00
Daniel Santana
96f961ef54 fix: queue_microtask without JS (#1145) 2023-06-05 15:23:04 -04:00
Greg Johnston
4ade062cd8 fix: erroneous reactivity warning at form.rs:96 (#1142) 2023-06-04 20:09:21 -04:00
itehax
53efcb989c examples: Tailwind + Leptos & Axum (#1111) 2023-06-03 16:55:47 -04:00
Lunatic
e183bfe278 docs: inculde missing cx in 16_routes.md (#1141) 2023-06-03 16:46:27 -04:00
yuuma03
51a6147609 feat: variable bindings on components (#1140) 2023-06-03 15:22:44 -04:00
martin frances
f6d856ee11 chore: cargo clippy --fix. (#1136) 2023-06-03 11:35:33 -04:00
Greg Johnston
4e41fad107 fix: wait for blocking fragments to resolve before pulling metadata (closes #1118) (#1137) 2023-06-02 17:32:32 -04:00
Greg Johnston
2bafdf2752 fix: remove nested fragments from Suspense (closes #1094 and #960) (#1135) 2023-06-02 11:40:49 -04:00
tempbottle
84e8922aa6 fix: remove need for inline JS for queue_microtask (#1129) 2023-06-02 08:16:29 -04:00
agilarity
53e09279a2 ci(examples): verify examples (#1125) 2023-06-01 22:12:18 -04:00
Vladimir Motylenko
38a1c1102f Closing tag highlight/hower and go-to definition support in lsp. (#1126) 2023-06-01 22:09:15 -04:00
Greg Johnston
c68df44717 chore: add guide for contributors (#1131) 2023-05-31 20:33:51 -04:00
Greg Johnston
55266f2efd perf: reduce overhead of hydration keys (#1122) 2023-05-31 20:30:31 -04:00
Greg Johnston
f3e544b003 chore: add discussions links to issue templates 2023-05-31 11:03:21 -04:00
Greg Johnston
e9054b01e6 chore: add issue templates (#1128) 2023-05-31 10:58:55 -04:00
Greg Johnston
d0660cf6da build: use cargo's sparse registry protocol (#1127) 2023-05-31 10:58:19 -04:00
Greg Johnston
8a27ca7c38 docs: add website link to README.md 2023-05-30 10:27:28 -04:00
Greg Johnston
ff86b2ef4f chore: fix warnings (#1123)
* chore: fix `hidden-glob-reexports` warning
* skip `template_macro` testing
2023-05-29 17:01:59 -04:00
Greg Johnston
1c236d74b6 docs: add examples of synchronizing signal values (#1121) 2023-05-29 07:38:06 -04:00
agilarity
af7e1d6a0f build(examples): auto install playwright browsers (#1114) 2023-05-28 18:01:38 -04:00
agilarity
dfd03d4f27 fix(examples): verification errors (#1113) (#1115)
* fix(examples/counters): unexpected each item comment

* fix(examples/errors_axum): format error

* fix(examples/ssr_modes_axum): format error

* fix(examples/ssr_modes_axum): unused import

* build(examples/timer): add common tasks

* fix(examples/timer) clippy error
2023-05-28 18:00:42 -04:00
Greg Johnston
5d70275c3a fix: dispose of runtime when stream is actually finished (closes #1097) (#1110) 2023-05-28 13:44:31 -04:00
Marc-Stefan Cassola
475566837e feat: added scrollend event (#1105) 2023-05-27 11:04:30 -04:00
Greg Johnston
a08cebbef9 examples: fix suspense scope in ssr_modes_axum (#1107) 2023-05-27 11:01:54 -04:00
Vladimir Motylenko
571e778bce fix: hygiene on template macro (#1101)
Pass dependency needed for template, and also hide them behind feature guide, to avoid compile time bloating.
2023-05-27 08:07:44 -04:00
Greg Johnston
2eb95395c8 Merge pull request #1106 from leptos-rs/gbj-patch-2
fix: remove debug logs accidentally included
2023-05-27 08:01:19 -04:00
Greg Johnston
7ff08615bb fix: remove debug logs accidentally included 2023-05-27 06:08:08 -04:00
Greg Johnston
3628aaab55 Merge pull request #1104 from leptos-rs/docs-updates
Docs updates
2023-05-26 17:06:58 -04:00
Greg Johnston
cd195c3700 docs: extractors 2023-05-26 17:06:07 -04:00
Abhik Jain
9dc5d93b99 docs: fix generic type (#1102) 2023-05-26 16:54:37 -04:00
Greg Johnston
f71e530810 docs: add leptos_meta section 2023-05-26 16:38:39 -04:00
Greg Johnston
6c471f7be4 docs: reorganize into a section on reactivity 2023-05-26 16:06:57 -04:00
Greg Johnston
f80f4ef110 docs: update Global State section for best practices 2023-05-26 16:00:37 -04:00
Greg Johnston
4d3dd7a6e6 feat: add Axum extract() function (#1093) 2023-05-25 11:16:58 -04:00
yuuma03
cc68d20758 fix: duplicate headers (like Set-Cookie) on the actix integration (#1086) 2023-05-25 11:16:29 -04:00
Matt Joiner
20682e63ef examples: fix fetch example (#1096) 2023-05-25 11:15:47 -04:00
Andrew Wheeler(Genusis)
40363df4a1 examples: updated axum_database_sessions to axum_session along with axum_sessions_auth to axum_session_auth (#1090) 2023-05-24 17:21:24 -04:00
Greg Johnston
e3ea889d5f feat: add <Await/> component to improve ergonomics of loading async blocks (#1091) 2023-05-24 14:05:36 -04:00
Greg Johnston
7f14da3026 fix: missing ? in navigation now that removed (#1092) 2023-05-24 12:12:57 -04:00
Ben Wishovich
06d28f7d67 feat: use Axum SubStates to enable .with_state in Axum router (#1085) 2023-05-24 08:34:17 -04:00
sjud
27f2a672ba docs: added a hint for a common error when using use_navigate (#1063) 2023-05-23 19:51:03 -04:00
Greg Johnston
23f9d537e9 fix: correctly handle new navigations while in the middle of an async navigation (#1084) 2023-05-23 17:21:12 -04:00
Rushi
d86339bae3 feat: manually implement Debug, PartialEq, Eq and Hash for reactive types (#1080) (closes #1074) 2023-05-22 16:52:59 -04:00
Greg Johnston
846c338491 docs: clarify difference between set() and update() (#1082) 2023-05-22 15:34:15 -04:00
Greg Johnston
2d418dae93 fix: debug-mode bugs in <For/> (closes #955, #1075, #1076) (#1078) 2023-05-22 06:49:13 -04:00
Greg Johnston
91e0fcdc1b fix/change: remove ? prefix from search in browser (matching server behavior) - closes #1071 (#1077) 2023-05-21 22:06:38 -04:00
Greg Johnston
a9ed8461d1 feat: add "async routing" feature (#1055)
* add "async routing" feature that waits for async resources to resolve before navigating
* add support for Outlet
* add `<RoutingProgress/>` component
2023-05-21 06:46:23 -04:00
Vladimir Motylenko
5a71ca797a feat: RSX parser with recovery after errors, and unquoted text (#1054)
* Feat: Upgrade to new local version of syn-rsx

* chore: Make macro more IDE friendly

1. Add quotation to RawText node.
2. Replace vec! macro with [].to_vec().
Cons:
1. Temporary remove allow(unused_braces) from expressions, to allow completion after dot in rust-analyzer.

* chore: Change dependency from syn-rsx to rstml

* chore: Fix value_to_string usage, pr comments, and fmt.
2023-05-21 06:45:53 -04:00
agilarity
70eb07d7d6 test: setup e2e automatically (#1067) 2023-05-20 20:46:06 -04:00
Greg Johnston
71ee69af01 fix: avoid potential already-borrowed issues with resources nested in suspense 2023-05-20 20:42:06 -04:00
Ben Wishovich
dd41c0586c feat: allow specifying exact server function paths (#1069) 2023-05-19 16:47:28 -04:00
Greg Johnston
aaf63dbf5c docs: clarify SSR/WASM binary size comments (#1070) 2023-05-19 15:46:26 -04:00
Greg Johnston
87f6802967 docs: update notes on WASM binary size to work with SSR too (closes #1059) (#1068) 2023-05-19 15:08:32 -04:00
Greg Johnston
2cbf3581c5 fix: docs note on style refers to class (#1066) 2023-05-19 13:42:16 -04:00
agilarity
5a67e208fd test: verify tailwind example with playwright tests (#1062)
* chore: ignore playwright output

* fix: could not run playwright test

* test: should see the welcome message

* build: clean playwright output

* build: run playwright web tests

* build: setup e2e dependencies
2023-05-19 13:04:06 -04:00
Greg Johnston
3391a4a035 examples: fix todo_app_sqlite_axum (#1064) 2023-05-19 13:02:52 -04:00
Daniel Santana
076aa363a4 feat: added Debug, PartialEq and Eq derives to trigger. (#1060) 2023-05-18 20:32:25 -04:00
agilarity
2cb68c0bd4 fix: todomvc example style errors (#1058) 2023-05-18 15:49:34 -04:00
Greg Johnston
6eb24b5017 tests: fix broken SSR doctests (#1056) 2023-05-18 10:17:14 -04:00
yuuma03
b2faa6b86c feat: allow multipart forms on server fns (Actix) (#1048) 2023-05-17 19:53:55 -04:00
sjud
43990b5b67 docs: include link to book, Discord, examples (#1053) 2023-05-17 13:07:17 -04:00
kasbuunk
9453164dd2 docs: fix typo in view fn (#1050) 2023-05-16 14:34:37 -04:00
Greg Johnston
00fcd1c65e docs: fix small docs issues (closes #1045) (#1049) 2023-05-16 13:01:29 -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
359 changed files with 12552 additions and 4255 deletions

39
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,39 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Leptos Dependencies**
Please copy and paste the Leptos dependencies and features from your `Cargo.toml`.
For example:
```toml
leptos = { version = "0.3", features = ["serde"] }
leptos_axum = { version = "0.3", optional = true }
leptos_meta = { version = "0.3"}
leptos_router = { version = "0.3"}
```
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

7
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
contact_links:
- name: Support or Question
url: https://github.com/leptos-rs/leptos/discussions/new?category=q-a
about: Do you need help figuring out how to do something, or want some help troubleshooting a bug? You can ask in our Discussions section.
- name: Discord Discussions
url: https://discord.gg/YdRAhS7eQB
about: For more informal, real-time conversation and support, you can join our Discord server.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,45 +1,46 @@
name: Test
name: Check Examples
on:
push:
branches: [main]
branches:
- main
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
branches:
- main
jobs:
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Install JQ Tool
uses: mbround18/install-jq@v1
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make check-examples
matrix-job:
name: Check
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"

View File

@@ -1,4 +1,4 @@
name: Test
name: Check stable
on:
push:
@@ -8,6 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:

View File

@@ -1,4 +1,4 @@
name: Test
name: Check
on:
push:
@@ -8,6 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:

View File

@@ -1,4 +1,4 @@
name: Test
name: Format
on:
push:

95
.github/workflows/run-example-task.yml vendored Normal file
View File

@@ -0,0 +1,95 @@
name: Run Example Task
on:
workflow_call:
inputs:
directory:
required: true
type: string
cargo_make_task:
required: true
type: string
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
# Setup environment
- name: Install playwright browser dependencies
run: |
sudo apt-get update
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Install Trunk
uses: jetli/trunk-action@v0.4.0
with:
version: "latest"
- name: Print Trunk Version
run: trunk --version
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
# Verify project
- name: ${{ inputs.cargo_make_task }}
run: |
if [ "${{ inputs.directory }}" = "INTERNAL" ]; then
echo No verification required
else
cd ${{ inputs.directory }}
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
fi

View File

@@ -8,6 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:

View File

@@ -0,0 +1,47 @@
name: Verify All Examples
on:
workflow_dispatch:
push:
tags:
- v*
schedule:
# Run once a day at 3:00 AM EST
- cron: "0 8 * * *"
jobs:
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install JQ Tool
uses: mbround18/install-jq@v1
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
matrix-job:
name: Verify
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

View File

@@ -0,0 +1,71 @@
name: Verify Changed Examples
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
setup:
name: Get Changes
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get all example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
with:
files: |
examples
- name: List all example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v36
with:
dir_names: true
dir_names_max_depth: "2"
files: |
examples
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml
!examples/README.md
json: true
quotepath: false
- name: List example project directories that changed
run: echo '${{ steps.changed-dirs.outputs.all_changed_files }}'
- name: Set Matrix
id: set-matrix
run: |
if [ ${{ steps.changed-files.outputs.any_changed }} == 'true' ]; then
# Create matrix with changed directories
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
else
# Create matrix with one item to prevent an empty vector error
echo "matrix={\"directory\":[\"INTERNAL\"]}" >> "$GITHUB_OUTPUT"
fi
matrix-job:
name: Verify
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

231
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,231 @@
# Architecture
The goal of this document is to make it easier for contributors (and anyone
whos interested!) to understand the architecture of the framework.
The whole Leptos framework is built from a series of layers. Each of these layers
depends on the one below it, but each can be used independently from the ones
built on top of it. While running a command like `cargo leptos new --git
leptos-rs/start` pulls in the whole framework, its important to remember that
none of this is magic: each layer of that onion can be stripped away and
reimplemented, configured, or adapted as needed, incrementally.
> Everything that follows will assume you have a good working understanding
> of the framework. There will be explanations of how some parts of it work
> or fit together, but these are not docs. They assume you know what Im
> talking about.
## The Reactive System: `leptos_reactive`
The reactive system allows you to define dynamic values (signals),
the relationships between them (derived signals and memos), and the side effects
that run in response to them (effects).
These concepts are completely independent of the DOM and can be used to drive
any kind of reactive updates. The reactive system is based on the assumption
that data is relatively cheap, and side effects are relatively expensive. Its
goal is to minimize those side effects (like updating the DOM or making a network
requests) as infrequently as possible.
The reactive system is implemented as a single data structure that exists at
runtime. In exchange for giving ownership over a value to the reactive system
(by creating a signal), you receive a `Copy + 'static` identifier for its
location in the reactive system. This enables most of the ergonomics of storing
and sharing state, the use of callback closures without lifetime issues, etc.
This is implemented by storing signals in a slotmap arena. The signal, memo,
and scope types that are exposed to users simply carry around an index into that
slotmap.
> Items owned by the reactive system are dropped when the corresponding reactive
> scope is dropped, i.e., when the component or section of the UI theyre
> created in is removed. In a sense, Leptos implements a “garbage collector”
> in which the lifetime of data is tied to the lifetime of the UI, not Rusts
> lexical scopes.
## The DOM Renderer: `leptos_dom`
The reactive system can be used to drive any kinds of side effects. One very
common side effect is calling an imperative method, for example to update the
DOM.
The entire DOM renderer is built on top of the reactive system. It provides
a builder pattern that can be used to create DOM elements dynamically.
The renderer assumes, as a convention, that dynamic attributes, classes,
styles, and children are defined by being passed a `Fn() -> T`, where their
static equivalents just receive `T`. Theres nothing about this that is
divinely ordained, but its a useful convention because it allows us to use
zero-overhead derived signals as one of several ways to indicate dynamic
content.
`leptos_dom` also contains code for server-side rendering of the same
UI views to HTML, either for out-of-order streaming (`src/ssr.rs`) or
in-order streaming/async rendering (`src/ssr_in_order.rs`).
## The Macros: `leptos_macro`
Its entirely possible to write Leptos code with no macros at all. The
`view` and `component` macros, the most common, can be replaced by
the builder syntax and simple functions (see the `counter_without_macros`
example). But the macros enable a JSX-like syntax for describing views.
This package also contains the `Params` derive macro used for typed
queries and route params in the router.
### Macro-based Optimizations
Leptos 0.0.x was built much more heavily on macros. Taking its cues
from SolidJS, the `view` macro emitted different code for CSR, SSR, and
hydration, optimizing each. The CSR/hydrate versions worked by compiling
the view to an HTML template string, cloning that `<template>`, and
traversing the DOM to set up reactivity. The SSR version worked similarly
by compiling the static parts of the view to strings at compile time,
reducing the amount of work that needed to be done on each request.
Proc macros are hard, and this system was brittle. 0.1 introduced a
more robust renderer, including the builder syntax, and rebuilt the `view`
macro to use that builder syntax instead. It moved the optimized-but-buggy
CSR version of the macro to a more-limited `template` macro.
The `view` macro now separately optimizes SSR to use the same static-string
optimizations, which (by our benchmarks) makes Leptos about 3-4x faster
than similar Rust frontend frameworks in its HTML rendering.
> The optimization is pretty straightforward. Consider the following view:
>
> ```rust
> view! { cx,
> <main class="text-center">
> <div class="flex-col">
> <button>"Click me."</button>
> <p class="italic">"Text."</p>
> </div>
> </main>
> }
> ```
>
> Internally, with the builder this is something like
>
> ```rust
> Element {
> tag: "main",
> attrs: vec![("class", "text-center")],
> children: vec![
> Element {
> tag: "div",
> attrs: vec![("class", "flex-col")],
> children: vec![
> Element {
> tag: "button",
> attrs: vec![],
> children: vec!["Click me"]
> },
> Element {
> tag: "p",
> attrs: vec![("class", "italic")],
> children: vec!["Text"]
> }
> ]
> }
> ]
> }
> ```
>
> This is a _bunch_ of small allocations and separate strings,
> and in early 0.1 versions we used a `SmallVec` for children and
> attributes and actually caused some stack overflows.
>
> But if you look at the view itself you can see that none of this
> will _ever_ change. So we can actually optimize it at compile
> time to a single `&'static str`:
>
> ```rust
> r#"<main class="text-center">
> <div class="flex-col">
> <button>"Click me."</button>
> <p class="italic">"Text."</p>
> </div>
> </main>"#
> ```
## Server Functions (`leptos_server`, `server_fn`, and `server_fn_macro`)
Server functions are a framework-agnostic shorthand for converting
a function, whose body can only be run on the server, into an ad hoc
REST API endpoint, and then generating code on the client to call that
endpoint when you call the function.
These are inspired by Solid/Blings `server$` functions, and theres
similar work being done in a number of other JavaScript frameworks.
RPC is not a new idea, but these kinds of server functions may be.
Specifically, by using web standards (defaulting to `POST`/`GET` requests
with URL-encoded form data) they allow easy graceful degradation and the
use of the `<form>` element.
This function is split across three packages so that `server_fn` and
`server_fn_macro` can be used by other frameworks. `leptos_server`
includes some Leptos-specific reactive functionality (like actions).
## `leptos`
This package is built on and reexports most of the layers already
mentioned, and implements a number of control-flow components (`<Show/>`,
`<ErrorBoundary/>`, `<For/>`, `<Suspense/>`, `<Transition/>`) that use
public APIs of the other packages.
This is the main entrypoint for users, but is relatively light itself.
## `leptos_meta`
This package exists to allow you to work with tags normally found in
the `<head>`, from within your components.
It is implemented as a distinct package, rather than part of
`leptos_dom`, on the principle that “what can be implemented in userland,
should be.” The framework can be used without it, so its not in core.
## `leptos_router`
The router originates as a direct port of `solid-router`, which is the
origin of most of its terminology, architecture, and route-matching logic.
Subsequent developments (like animated routing, and managing route transitions
given the lack of `useTransition` in Leptos) have caused it to diverge
slightly from Solids exact code, but it is still very closely related.
The core principle here is “nested routing,” dividing a single page
into independently-rendered parts. This is described in some detail in the docs.
Like `leptos_meta`, it is implemented as a distinct package, because it
can be replaced with another router or with none. The framework can be used
without it, so its not in core.
## Server Integrations
The server integrations are the most “frameworky” layer of the whole framework.
These **do** assume the use of `leptos`, `leptos_router`, and `leptos_meta`.
They specifically draw routing data from `leptos_router`, and inject the
metadata from `leptos_meta` into the `<head>` appropriately.
But of course, if you one day create `leptos-helmet` and `leptos-better-router`,
you can create new server integrations that plug them into the SSR rendering
methods from `leptos_dom` instead. Everything involved is quite modular.
These packages essentially provide helpers that save the templates and user apps
from including a huge amount of boilerplate to connect the various other packages
correctly. Again, early versions of the framework examples are illustrative here
for reference: they include large amounts of manual SSR route handling, etc.
## `cargo-leptos` helpers
`leptos_config` and `leptos_hot_reload` exist to support two different features
of `cargo-leptos`, namely its configuration and its view-patching/hot-
reloading features.
Its important to say that the main feature `cargo-leptos` remains its ability
to conveniently tie together different build tooling, compiling your app to
WASM for the browser, building the server version, pulling in SASS and
Tailwind, etc. It is an extremely good build tool, not a magic formula. Each
of the examples includes instructions for how to run the examples without
`cargo-leptos`.

View File

@@ -2,7 +2,7 @@
_This Code of Conduct is based on the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)
and the [Bevy Code of Conduct](https://raw.githubusercontent.com/bevyengine/bevy/main/CODE_OF_CONDUCT.md),
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
and the [Contributor Covenant](https://www.contributor-covenant.org)._
## Our Pledge

75
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,75 @@
# Contributing to Leptos
Thanks for your interesting in contributing to Leptos! This is a truly
community-driven framework, and while we have a central maintainer (@gbj)
large parts of the renderer, reactive system, and server integrations have
all been written by other contributors. Contributions are always welcome.
Participation in this community is governed by a [Code of Conduct](./CODE_OF_CONDUCT.md).
Some of the most active conversations around development take place on our
[Discord server](https://discord.gg/YdRAhS7eQB).
This guide seeks to
- describe some of the frameworks values (in a technical, not an ethical, sense)
- provide a high-level overview of how the pieces of the framework fit together
- orient you to the organization of this repository
## Values
Leptos, as a framework, reflects certain technical values:
- **Expose primitives rather than imposing patterns.** Provide building blocks
that users can combine together to build up more complex behavior, rather than
requiring users follow certain templates, file formats, etc. e.g., components
are defined as functions, rather than a bespoke single-file component format.
The reactive system feeds into the rendering system, rather than being defined
by it.
- **Bottom-up over top-down.** If you envision a users application as a tree
(like an HTML document), push meaning toward the leaves of the tree. e.g., If data
needs to be loaded, load it in a granular primitive (resources) rather than a
route- or page-level data structure.
- **Performance by default.** When possible, users should only pay for what they
use. e.g., we dont make all component props reactive by default. This is
because doing so would force the overhead of a reactive prop onto props that dont
need to be reactive.
- **Full-stack performance.** Performance cant be limited to a single metric,
whether thats a DOM rendering benchmark, WASM binary size, or server response
time. Use methods like HTTP streaming and progressive enhancement to enable
applications to load, become interactive, and respond as quickly as possible.
- **Use safe Rust.** Theres no need for `unsafe` Rust in the framework, and
avoiding it at all costs reduces the maintenance and testing burden significantly.
- **Embrace Rust semantics.** Especially in things like UI templating, use Rust
semantics or extend them in a predictable way with control-flow components
rather than overloading the meaning of Rust terms like `if` or `for` in a
framework-specific way.
- **Enhance ergonomics without obfuscating whats happening.** This is by far
the hardest to achieve. Its often the case that adding additional layers to
improve DX (like a custom build tool and starter templates) comes across as
“too magic” to some people who havent had to build the same things manually.
When possible, make it easier to see how the pieces fit together, without
sacrificing the improved DX.
## Processes
We do not have PR templates or formal processes for approving PRs. But there
are a few guidelines that will make it a better experience for everyone:
- Run `cargo fmt` before submitting your code.
- Keep PRs limited to addressing one feature or one issue, in general. In some
cases (e.g., “reduce allocations in the reactive system”) this may touch a number
of different areas, but is still conceptually one thing.
- If its an unsolicited PR not linked to an open issue, please include a
specific explanation for what its trying to achieve. For example: “When I
was trying to deploy my app under _circumstances X_, I found that the way
_function Z_ was implemented caused _issue Z_. This PR should fix that by
_solution._
- Our CI tests every PR against all the existing examples, sometimes requiring
compilation for both server and client side, etc. Its thorough but slow. If
you want to run CI locally to reduce frustration, you can do that by installing
`cargo-make` and using `cargo make check && cargo make test && cargo make
check-examples`.
## Architecture
See [ARCHITECTURE.md](./ARCHITECTURE.md).

View File

@@ -1,4 +1,5 @@
[workspace]
resolver = "2"
members = [
# core
"leptos",
@@ -25,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.3.0-alpha"
version = "0.4.2"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.3.0-alpha" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0-alpha" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0-alpha" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0-alpha" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0-alpha" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0-alpha" }
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0-alpha" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0-alpha" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0-alpha" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0-alpha" }
leptos_router = { path = "./router", version = "0.3.0-alpha" }
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0-alpha" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0-alpha" }
leptos = { path = "./leptos", version = "0.4.2" }
leptos_dom = { path = "./leptos_dom", version = "0.4.2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.2" }
leptos_macro = { path = "./leptos_macro", version = "0.4.2" }
leptos_reactive = { path = "./leptos_reactive", version = "0.4.2" }
leptos_server = { path = "./leptos_server", version = "0.4.2" }
server_fn = { path = "./server_fn", version = "0.4.2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.4.2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.2" }
leptos_config = { path = "./leptos_config", version = "0.4.2" }
leptos_router = { path = "./router", version = "0.4.2" }
leptos_meta = { path = "./meta", version = "0.4.2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.2" }
[profile.release]
codegen-units = 1

View File

@@ -45,6 +45,8 @@ dependencies = [
{ name = "check", path = "examples/fetch" },
{ name = "check", path = "examples/hackernews" },
{ name = "check", path = "examples/hackernews_axum" },
{ name = "check", path = "examples/js-framework-benchmark" },
{ name = "check", path = "examples/leptos-tailwind-axum" },
{ name = "check", path = "examples/login_with_token_csr_only" },
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
@@ -54,6 +56,7 @@ dependencies = [
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/tailwind_csr_trunk" },
{ name = "check", path = "examples/timer" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todo_app_sqlite_viz" },
@@ -69,7 +72,11 @@ dependencies = [
[tasks.test]
clear = true
dependencies = ["test-all", "test-leptos_macro-example", "doc-leptos_macro-example"]
dependencies = [
"test-all",
"test-leptos_macro-example",
"doc-leptos_macro-example",
]
[tasks.test-all]
command = "cargo"
@@ -98,13 +105,20 @@ args = ["make", "test-unit-and-web"]
[tasks.verify-examples]
description = "Run all quality checks and tests for examples"
env = { CLEAN_AFTER_VERIFY = "true" }
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
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"

View File

@@ -8,6 +8,8 @@
[![Discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord)](https://discord.gg/YdRAhS7eQB)
[![Matrix](https://img.shields.io/badge/Matrix-leptos-grey?logo=matrix&labelColor=white&logoColor=black)](https://matrix.to/#/#leptos:matrix.org)
[Website](https://leptos.dev) | [Book](https://leptos-rs.github.io/leptos/) | [Docs.rs](https://docs.rs/leptos/latest/leptos/) | [Playground](https://codesandbox.io/p/sandbox/leptos-rtfggt?file=%2Fsrc%2Fmain.rs%3A1%2C1) | [Discord](https://discord.gg/YdRAhS7eQB)
# Leptos
```rust
@@ -66,7 +68,7 @@ Here are some resources for learning more about Leptos:
## `nightly` Note
Most of the examples assume youre using `nightly` version of Rust. For this, you can either set your toolchain globally or on per-project basis.
Most of the examples assume youre using `nightly` version of Rust and the `nightly` feature of Leptos. To use `nightly` Rust, you can either set your toolchain globally or on per-project basis.
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you havent already):
@@ -84,13 +86,9 @@ channel = "nightly"
targets = ["wasm32-unknown-unknown"]
```
If youre on `stable`, note the following:
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`,
youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
for examples of the correct API.
> Note: The `nightly` feature is present on the main branch version right now, but not in 0.3.x. For 0.3.x, nightly is the default and `stable` has a special feature.
## `cargo-leptos`

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
l021 = { package = "leptos", version = "0.2.1" }
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
leptos = { path = "../leptos", features = ["ssr"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
tokio-test = "0.4"

View File

@@ -25,13 +25,18 @@ cargo init leptos-tutorial
> ```bash
> rustup toolchain install nightly
> rustup default nightly
> rustup target add wasm32-unknown-unknown
> ```
Make sure you've added the `wasm32-unknown-unknown` target do that Rust can compile your code to WebAssembly to run in the browser.
```bash
rustup target add wasm32-unknown-unknown
```
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
```
Create a simple `index.html` in the root of the `leptos-tutorial` directory

View File

@@ -1,112 +1 @@
# Responding to Changes with `create_effect`
Believe it or not, weve made it this far without having mentioned half of the reactive system: effects.
Leptos is built on a fine-grained reactive system, which means that individual reactive values (“signals,” sometimes known as observables) trigger rerunning the code that reacts to them (“effects,” sometimes known as observers). These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as theres no observable value to subscribe to.
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
```rust
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
create_effect(cx, move |_| {
// immediately prints "Value: 0" and subscribes to `a`
log::debug!("Value: {}", a());
});
```
The effect function is called with an argument containing whatever value it returned the last time it ran. On the initial run, this is `None`.
By default, effects **do not run on the server**. This means you can call browser-specific APIs within the effect function without causing issues. If you need an effect to run on the server, use [`create_isomorphic_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_isomorphic_effect.html).
## Autotracking and Dynamic Dependencies
If youre familiar with a framework like React, you might notice one key difference. React and similar frameworks typically require you to pass a “dependency array,” an explicit set of variables that determine when the effect should rerun.
Because Leptos comes from the tradition of synchronous reactive programming, we dont need this explicit dependency list. Instead, we automatically track dependencies depending on which signals are accessed within the effect.
This has two effects (no pun intended). Dependencies are
1. **Automatic**: You dont need to maintain a dependency list, or worry about what should or shouldnt be included. The framework simply tracks which signals might cause the effect to rerun, and handles it for you.
2. **Dynamic**: The dependency list is cleared and updated every time the effect runs. If your effect contains a conditional (for example), only signals that are used in the current branch are tracked. This means that effects rerun the absolute minimum number of times.
> If this sounds like magic, and if you want a deep dive into how automatic dependency tracking works, [check out this video](https://www.youtube.com/watch?v=GWB3vTWeLd4). (Apologies for the low volume!)
## Effects as Zero-Cost-ish Abstraction
While theyre not a “zero-cost abstraction” in the most technical sense—they require some additional memory use, exist at runtime, etc.—at a higher level, from the perspective of whatever expensive API calls or other work youre doing within them, effects are a zero-cost abstraction. They rerun the absolute minimum number of times necessary, given how youve described them.
Imagine that Im creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
```rust
let (first, set_first) = create_signal(cx, String::new());
let (last, set_last) = create_signal(cx, String::new());
let (use_last, set_use_last) = create_signal(cx, true);
// this will add the name to the log
// any time one of the source signals changes
create_effect(cx, move |_| {
log(
cx,
if use_last() {
format!("{} {}", first(), last())
} else {
first()
},
)
});
```
If `use_last` is `true`, effect should rerun whenever `first`, `last`, or `use_last` changes. But if I toggle `use_last` to `false`, a change in `last` will never cause the full name to change. In fact, `last` will be removed from the dependency list until `use_last` toggles again. This saves us from sending multiple unnecessary requests to the API if I change `last` multiple times while `use_last` is still `false`.
## To `create_effect`, or not to `create_effect`?
Effects are intended to run _side-effects_ of the system, not to synchronize state _within_ the system. In other words: dont write to signals within effects.
If you need to define a signal that depends on the value of other signals, use a derived signal or [`create_memo`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_memo.html).
If you need to synchronize some reactive value with the non-reactive world outside—like a web API, the console, the filesystem, or the DOM—create an effect.
> If youre curious for more information about when you should and shouldnt use `create_effect`, [check out this video](https://www.youtube.com/watch?v=aQOFJQ2JkvQ) for a more in-depth consideration!
## Effects and Rendering
Weve managed to get this far without mentioning effects because theyre built into the Leptos DOM renderer. Weve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
```rust
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<p>{count}</p>
}
```
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
```rust
let (count, set_count) = create_signal(cx, 0);
// create a DOM element
let p = create_element("p");
// create an effect to reactively update the text
create_effect(cx, move |prev_value| {
// first, access the signals value and convert it to a string
let text = count().to_string();
// if this is different from the previous value, update the node
if prev_value != Some(text) {
p.set_text_content(&text);
}
// return this value so we can memoize the next update
text
});
```
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
# Responding to Changes with create_effect

View File

@@ -1,8 +1,8 @@
# Global State Management
So far, we've only been working with local state in components
We've only seen how to communicate between parent and child components
But there are also more general ways to manage global state
So far, we've only been working with local state in components, and weve seen how to coordinate state between parent and child components. On occasion, there are times where people look for a more general solution for global state management that can work throughout an application.
In general, **you do not need this chapter.** The typical pattern is to compose your application out of components, each of which manages its own local state, not to store all state in a global structure. However, there are some cases (like theming, saving user settings, or sharing data between components in different parts of your UI) in which you may want to use some kind of global state management.
The three best approaches to global state are
@@ -12,19 +12,17 @@ The three best approaches to global state are
## Option #1: URL as Global State
The next few sections of the tutorial will be about the router.
So for now, we'll just look at options #2 and #3.
In many ways, the URL is actually the best way to store global state. It can be accessed from any component, anywhere in your tree. There are native HTML elements like `<form>` and `<a>` that exist solely to update the URL. And it persists across page reloads and between devices; you can share a URL with a friend or send it from your phone to your laptop and any state stored in it will be replicated.
The next few sections of the tutorial will be about the router, and well get much more into these topics.
But for now, we'll just look at options #2 and #3.
## Option #2: Passing Signals through Context
In virtual DOM libraries like React, using the Context API to manage global
state is a bad idea: because the entire app exists in a tree, changing
some value provided high up in the tree can cause the whole app to render.
In the section on [parent-child communication](view/08_parent_child.md), we saw that you can use `provide_context` to pass signal from a parent component to a child, and `use_context` to read it in the child. But `provide_context` works across any distance. If you want to create a global signal that holds some piece of state, you can provide it and access it via context anywhere in the descendants of the component where you provide it.
In fine-grained reactive libraries like Leptos, this is simply not the case.
You can create a signal in the root of your app and pass it down to other
components using provide_context(). Changing it will only cause rerendering
in the specific places it is actually used, not the whole app.
A signal provided via context only causes reactive updates where it is read, not in any of the components in between, so it maintains the power of fine-grained reactive updates, even at a distance.
We start by creating a signal in the root of the app and providing it to
all its children and descendants using `provide_context`.
@@ -81,61 +79,72 @@ fn FancyMath(cx: Scope) -> impl IntoView {
}
```
This kind of “provide a signal in a parent, consume it in a child” should be familiar
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
pattern you use to communicate between parents and children works for grandparents and
grandchildren, or any ancestors and descendants: in other words, between “global” state
in the root component of your app and any other components anywhere else in the app.
Because of the fine-grained nature of updates, this is usually all you need. However,
in some cases with more complex state changes, you may want to use a slightly more
structured approach to global state.
## Option #3: Create a Global State Struct
You can use this approach to build a single global data structure
that holds the state for your whole app, and then access it by
taking fine-grained slices using
[`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html)
or [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html),
so that changing one part of the state doesn't cause parts of your
app that depend on other parts of the state to change.
You can begin by defining a simple state struct:
Note that this same pattern can be applied to more complex state. If you have multiple fields you want to update independently, you can do that by providing some struct of signals:
```rust
#[derive(Default, Clone, Debug)]
#[derive(Copy, Clone, Debug)]
struct GlobalState {
count: u32,
name: String,
count: RwSignal<i32>,
name: RwSignal<String>
}
```
Provide it in the root of your app so its available everywhere.
impl GlobalState {
pub fn new(cx: Scope) -> Self {
Self {
count: create_rw_signal(cx, 0),
name: create_rw_signal(cx, "Bob".to_string())
}
}
}
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
// we'll provide a single signal that holds the whole state
// each component will be responsible for creating its own "lens" into it
let state = create_rw_signal(cx, GlobalState::default());
provide_context(cx, state);
provide_context(cx, GlobalState::new(cx));
// ...
// etc.
}
```
Then child components can access “slices” of that state with fine-grained
updates via `create_slice`. Each slice signal only updates when the particular
piece of the larger struct it accesses updates. This means you can create a single
root signal, and then take independent, fine-grained slices of it in different
components, each of which can update without notifying the others of changes.
## Option #3: Create a Global State Struct and Slices
You may find it cumbersome to wrap each field of a structure in a separate signal like this. In some cases, it can be useful to create a plain struct with non-reactive fields, and then wrap that in a signal.
```rust
#[derive(Copy, Clone, Debug, Default)]
struct GlobalState {
count: i32,
name: String
}
#[component]
fn App(cx: Scope) -> impl IntoView {
provide_context(cx, create_rw_signal(GlobalState::default()));
// etc.
}
```
But theres a problem: because our whole state is wrapped in one signal, updating the value of one field will cause reactive updates in parts of the UI that only depend on the other.
```rust
let state = expect_context::<RwSignal<GlobalState>>(cx);
view! { cx,
<button on:click=move |_| state.update(|n| *n += 1)>"+1"</button>
<p>{move || state.with(|state| state.name.clone())}</p>
}
```
In this example, clicking the button will cause the text inside `<p>` to be updated, cloning `state.name` again! Because signals are the atomic unit of reactivity, updating any field of the signal triggers updates to everything that depends on the signal.
Theres a better way. You can use take fine-grained, reactive slices by using [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html) or [`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html) (which uses `create_memo` but also provides a setter). “Memoizing” a value means creating a new reactive value which will only update when it changes. “Memoizing a slice” means creating a new reactive value which will only update when some field of the state struct updates.
Here, instead of reading from the state signal directly, we create “slices” of that state with fine-grained updates via `create_slice`. Each slice signal only updates when the particular piece of the larger struct it accesses updates. This means you can create a single root signal, and then take independent, fine-grained slices of it in different components, each of which can update without notifying the others of changes.
```rust
/// A component that updates the count in the global state.
#[component]
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
let state = expect_context::<RwSignal<GlobalState>>(cx);
// `create_slice` lets us create a "lens" into the data
let (count, set_count) = create_slice(
@@ -169,6 +178,8 @@ somewhere else that only takes `state.name`, clicking the button wont cause
that other slice to update. This allows you to combine the benefits of a top-down
data flow and of fine-grained reactive updates.
> **Note**: There are some significant drawbacks to this approach. Both signals and memos need to own their values, so a memo will need to clone the fields value on every change. The most natural way to manage state in a framework like Leptos is always to provide signals that are as locally-scoped and fine-grained as they can be, not to hoist everything up into global state. But when you _do_ need some kind of global state, `create_slice` can be a useful tool.
[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

@@ -12,7 +12,10 @@
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.md)
- [Interlude: Reactivity and Functions](./interlude_functions.md)
- [Reactivity](./reactivity/README.md)
- [Working with Signals](./reactivity/working_with_signals.md)
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
- [Interlude: Reactivity and Functions](./reactivity/interlude_functions.md)
- [Testing](./testing.md)
- [Async](./async/README.md)
- [Loading Data with Resources](./async/10_resources.md)
@@ -29,23 +32,17 @@
- [`<A/>`](./router/19_a.md)
- [`<Form/>`](./router/20_form.md)
- [Interlude: Styling](./interlude_styling.md)
- [Metadata]()
- [Metadata](./metadata.md)
- [Server Side Rendering](./ssr/README.md)
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
- [The Life of a Page Load](./ssr/22_life_cycle.md)
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
- [Hydration Footguns](./ssr/24_hydration_bugs.md)
- [Server Functions]()
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
- [Headers]()
- [Cookies]()
- [Building Full-Stack Apps]()
- [Actions]()
- [Forms]()
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Advanced Reactivity]()
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
- [Working with the Server](./server/README.md)
- [Server Functions](./server/25_server_functions.md)
- [Extractors](./server/26_extractors.md)
- [Responses and Redirects](./server/27_response.md)
- [Progressive Enhancement and Graceful Degradation](./progressive_enhancement/README.md)
- [`<ActionForm/>`s](./progressive_enhancement/action_form.md)
- [Deployment]()
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)

View File

@@ -41,6 +41,20 @@ build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]
```
Note that if you're using this with SSR too, the same Cargo profile will be applied. You'll need to explicitly specify your target:
```toml
[build]
target = "x86_64-unknown-linux-gnu" # or whatever
```
Also note that in some cases, the cfg feature `has_std` will not be set, which may cause build errors with some dependencies which check for `has_std`. You may fix any build errors due to this by adding:
```toml
[build]
rustflags = ["--cfg=has_std"]
```
And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`. Note that this applies the same `build-std` and panic settings to your server binary, which may not be desirable. Some further exploration is probably needed here.
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

View File

@@ -11,7 +11,7 @@ Actions and resources seem similar, but they represent fundamentally different t
Say we have some `async` function we want to run.
```rust
async fn add_todo(new_title: &str) -> Uuid {
async fn add_todo_request(new_title: &str) -> Uuid {
/* do some stuff on the server to add a new todo */
}
```
@@ -41,16 +41,16 @@ async fn add_todo(new_title: &str) -> Uuid {
So in this case, all we need to do to create an action is
```rust
let add_todo = create_action(cx, |input: &String| {
let add_todo_action = create_action(cx, |input: &String| {
let input = input.to_owned();
async move { add_todo(&input).await }
async move { add_todo_request(&input).await }
});
```
Rather than calling `add_todo` directly, well call it with `.dispatch()`, as in
Rather than calling `add_todo_action` directly, well call it with `.dispatch()`, as in
```rust
add_todo.dispatch("Some value".to_string());
add_todo_action.dispatch("Some value".to_string());
```
You can do this from an event listener, a timeout, or anywhere; because `.dispatch()` isnt an `async` function, it can be called from a synchronous context.
@@ -58,9 +58,9 @@ You can do this from an event listener, a timeout, or anywhere; because `.dispat
Actions provide access to a few signals that synchronize between the asynchronous action youre calling and the synchronous reactive system:
```rust
let submitted = add_todo.input(); // RwSignal<Option<String>>
let pending = add_todo.pending(); // ReadSignal<bool>
let todo_id = add_todo.value(); // RwSignal<Option<Uuid>>
let submitted = add_todo_action.input(); // RwSignal<Option<String>>
let pending = add_todo_action.pending(); // ReadSignal<bool>
let todo_id = add_todo_action.value(); // RwSignal<Option<Uuid>>
```
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
@@ -73,7 +73,7 @@ view! { cx,
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
let input = input_ref.get().expect("input to exist");
add_todo.dispatch(input.value());
add_todo_action.dispatch(input.value());
}
>
<label>

View File

@@ -2,8 +2,8 @@
So far weve only been working with synchronous users interfaces: You provide some input,
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.
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

@@ -29,9 +29,9 @@ where
}
```
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.
This is pretty straightforward: when the user is logged in, we want to show `children`. 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.”
In other words, we want to pass the children of `<LoggedIn/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
This wont compile.

49
docs/book/src/metadata.md Normal file
View File

@@ -0,0 +1,49 @@
# Metadata
So far, everything weve rendered has been inside the `<body>` of the HTML document. And this makes sense. After all, everything you can see on a web page lives inside the `<body>`.
However, there are plenty of occasions where you might want to update something inside the `<head>` of the document using the same reactive primitives and component patterns you use for your UI.
Thats where the [`leptos_meta`](https://docs.rs/leptos_meta/latest/leptos_meta/) package comes in.
## Metadata Components
`leptos_meta` provides special components that let you inject data from inside components anywhere in your application into the `<head>`:
[`<Title/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Title.html) allows you to set the documents title from any component. It also takes a `formatter` function that can be used to apply the same format to the title set by other pages. So, for example, if you put `<Title formatter=|text| format!("{text} — My Awesome Site")/>` in your `<App/>` component, and then `<Title text="Page 1"/>` and `<Title text="Page 2"/>` on your routes, youll get `Page 1 — My Awesome Site` and `Page 2 — My Awesome Site`.
[`<Link/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Link.html) takes the standard attributes of the `<link>` element.
[`<Stylesheet/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Stylesheet.html) creates a `<link rel="stylesheet">` with the `href` you give.
[`<Style/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Style.html) creates a `<style>` with the children you pass in (usually a string). You can use this to import some custom CSS from another file at compile time `<Style>{include_str!("my_route.css")}</Style>`.
[`<Meta/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Meta.html) lets you set `<meta>` tags with descriptions and other metadata.
## `<Script/>` and `<script>`
`leptos_meta` also provides a [`<Script/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Script.html) component, and its worth pausing here for a second. All of the other components weve considered inject `<head>`-only elements in the `<head>`. But a `<script>` can also be included in the body.
Theres a very simple way to determine whether you should use a capital-S `<Script/>` component or a lowercase-s `<script>` element: the `<Script/>` component will be rendered in the `<head>`, and the `<script>` element will be rendered wherever in the `<body>` of your user interface you put it in, alongside other normal HTML elements. These cause JavaScript to load and run at different times, so use whichever is appropriate to your needs.
## `<Body/>` and `<Html/>`
There are even a couple elements designed to make semantic HTML and styling easier. [`<Html/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) lets you set the `lang` and `dir` on your `<html>` tag from your application code. `<Html/>` and [`<Body/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) both have `class` props that let you set their respective `class` attributes, which is sometimes needed by CSS frameworks for styling.
`<Body/>` and `<Html/>` both also have `attributes` props which can be used to set any number of additional attributes on them via the [`AdditionalAttributes`](https://docs.rs/leptos/latest/leptos/struct.AdditionalAttributes.html) type:
```rust
<Html
lang="he"
dir="rtl"
attributes=AdditionalAttributes::from(vec![("data-theme", "dark")])
/>
```
## Metadata and Server Rendering
Now, some of this is useful in any scenario, but some of it is especially important for search-engine optimization (SEO). Making sure you have things like appropriate `<title>` and `<meta>` tags is crucial. Modern search engine crawlers do handle client-side rendering, i.e., apps that are shipped as an empty `index.html` and rendered entirely in JS/WASM. But they prefer to receive pages in which your app has been rendered to actual HTML, with metadata in the `<head>`.
This is exactly what `leptos_meta` is for. And in fact, during server rendering, this is exactly what it does: collect all the `<head>` content youve declared by using its components throughout your application, and then inject it into the actual `<head>`.
But Im getting ahead of myself. We havent actually talked about server-side rendering yet. As a matter of fact... Lets do that next!

View File

@@ -0,0 +1,36 @@
# Progressive Enhancement (and Graceful Degradation)
Ive been driving around Boston for about fifteen years. If you dont know Boston, let me tell you: Massachusetts has some of the most aggressive drivers(and pedestrians!) in the world. Ive learned to practice whats sometimes called “defensive driving”: assuming that someones about to swerve in front of you at an intersection when you have the right of way, preparing for a pedestrian to cross into the street at any moment, and driving accordingly.
“Progressive enhancement” is the “defensive driving” of web design. Or really, thats “graceful degradation,” although theyre two sides of the same coin, or the same process, from two different directions.
**Progressive enhancement**, in this context, means beginning with a simple HTML site or application that works for any user who arrives at your page, and gradually enhancing it with layers of additional features: CSS for styling, JavaScript for interactivity, WebAssembly for Rust-powered interactivity; using particular Web APIs for a richer experience if theyre available and as needed.
**Graceful degradation** means handling failure gracefully when parts of that stack of enhancement *arent* available. Here are some sources of failure your users might encounter in your app:
- Their browser doesnt support WebAssembly because it needs to be updated.
- Their browser cant support WebAssembly because browser updates are limited to newer OS versions, which cant be installed on the device. (Looking at you, Apple.)
- They have WASM turned off for security or privacy reasons.
- They have JavaScript turned off for security or privacy reasons.
- JavaScript isnt supported on their device (for example, some accessibility devices only support HTML browsing)
- The JavaScript (or WASM) never arrived at their device because they walked outside and lost WiFi.
- They stepped onto a subway car after loading the initial page and subsequent navigations cant load data.
- ... and so on.
How much of your app still works if one of these holds true? Two of them? Three?
If the answer is something like “95%... okay, then 90%... okay, then 75%,” thats graceful degradation. If the answer is “my app shows a blank screen unless everything works correctly,” thats... rapid unscheduled disassembly.
**Graceful degradation is especially important for WASM apps,** because WASM is the newest and least-likely-to-be-supported of the four languages that run in the browser (HTML, CSS, JS, WASM).
Luckily, weve got some tools to help.
## Defensive Design
There are a few practices that can help your apps degrade more gracefully:
1. **Server-side rendering.** Without SSR, your app simply doesnt work without both JS and WASM loading. In some cases this may be appropriate (think internal apps gated behind a login) but in others its simply broken.
2. **Native HTML elements.** Use HTML elements that do the things that you want, without additional code: `<a>` for navigation (including to hashes within the page), `<details>` for an accordion, `<form>` to persist information in the URL, etc.
3. **URL-driven state.** The more of your global state is stored in the URL (as a route param or part of the query string), the more of the page can be generated during server rendering and updated by an `<a>` or a `<form>`, which means that not only navigations but state changes can work without JS/WASM.
4. **[`SsrMode::PartiallyBlocked` or `SsrMode::InOrder`](https://docs.rs/leptos_router/latest/leptos_router/enum.SsrMode.html).** Out-of-order streaming requires a small amount of inline JS, but can fail if 1) the connection is broken halfway through the response or 2) the clients device doesnt support JS. Async streaming will give a complete HTML page, but only after all resources load. In-order streaming begins showing pieces of the page sooner, in top-down order. “Partially-blocked” SSR builds on out-of-order streaming by replacing `<Suspense/>` fragments that read from blocking resources on the server. This adds marginally to the initial response time (because of the `O(n)` string replacement work), in exchange for a more complete initial HTML response. This can be a good choice for situations in which theres a clear distinction between “more important” and “less important” content, e.g., blog post vs. comments, or product info vs. reviews. If you choose to block on all the content, youve essentially recreated async rendering.
5. **Leaning on `<form>`s.** Theres been a bit of a `<form>` renaissance recently, and its no surprise. The ability of a `<form>` to manage complicated `POST` or `GET` requests in an easily-enhanced way makes it a powerful tool for graceful degradation. The example in [the `<Form/>` chapter](../router/20_form.md), for example, would work fine with no JS/WASM: because it uses a `<form method="GET">` to persist state in the URL, it works with pure HTML by making normal HTTP requests and then progressively enhances to use client-side navigations instead.
Theres one final feature of the framework that we havent seen yet, and which builds on this characteristic of forms to build powerful applications: the `<ActionForm/>`.

View File

@@ -0,0 +1,56 @@
# `<ActionForm/>`
[`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) is a specialized `<Form/>` that takes a server action, and automatically dispatches it on form submission. This allows you to call a server function directly from a `<form>`, even without JS/WASM.
The process is simple:
1. Define a server function using the [`#[server]` macro](https://docs.rs/leptos/latest/leptos/attr.server.html) (see [Server Functions](../server/25_server_functions.md).)
2. Create an action using [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), specifying the type of the server function youve defined.
3. Create an `<ActionForm/>`, providing the server action in the `action` prop.
4. Pass the named arguments to the server function as form fields with the same names.
> **Note:** `<ActionForm/>` only works with the default URL-encoded `POST` encoding for server functions, to ensure graceful degradation/correct behavior as an HTML form.
```rust
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
todo!()
}
#[component]
fn AddTodo(cx: Scope) -> impl IntoView {
let add_todo = create_server_action::<AddTodo>(cx);
// holds the latest *returned* value from the server
let value = add_todo.value();
// check if the server has returned an error
let has_error = move || value.with(|val| matches!(val, Some(Err(_))));
view! { cx,
<ActionForm action=add_todo>
<label>
"Add a Todo"
// `title` matches the `title` argument to `add_todo`
<input type="text" name="title"/>
</label>
<input type="submit" value="Add"/>
</ActionForm>
}
}
```
Its really that easy. With JS/WASM, your form will submit without a page reload, storing its most recent submission in the `.input()` signal of the action, its pending status in `.pending()`, and so on. (See the [`Action`](https://docs.rs/leptos/latest/leptos/struct.Action.html) docs for a refresher, if you need.) Without JS/WASM, your form will submit with a page reload. If you call a `redirect` function (from `leptos_axum` or `leptos_actix`) it will redirect to the correct page. By default, it will redirect back to the page youre currently on. The power of HTML, HTTP, and isomorphic rendering mean that your `<ActionForm/>` simply works, even with no JS/WASM.
## Client-Side Validation
Because the `<ActionForm/>` is just a `<form>`, it fires a `submit` event. You can use either HTML validation, or your own client-side validation logic in an `on:submit`. Just call `ev.prevent_default()` to prevent submission.
The [`FromFormData`](https://docs.rs/leptos_router/latest/leptos_router/trait.FromFormData.html) trait can be helpful here, for attempting to parse your server functions data type from the submitted form.
```rust
let on_submit = move |ev| {
let data = AddTodo::from_event(&ev);
// silly example of validation: if the todo is "nope!", nope it
if data.is_err() || data.unwrap().title == "nope!" {
// ev.prevent_default() will prevent form submission
ev.prevent_default();
}
}
```

View File

@@ -0,0 +1,114 @@
# Responding to Changes with `create_effect`
Weve made it this far without having mentioned half of the reactive system: effects.
Reactivity works in two halves: updating individual reactive values (“signals”) notifies the pieces of code that depend on them (“effects”) that they need to run again. These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as theres no observable value to subscribe to. Effects are quite literally “side effects” of the reactive system: they exist to synchronize the reactive system with the non-reactive world outside it.
Hidden behind the whole reactive DOM renderer that weve seen so far is a function called `create_effect`.
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
```rust
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
create_effect(cx, move |_| {
// immediately prints "Value: 0" and subscribes to `a`
log::debug!("Value: {}", a());
});
```
The effect function is called with an argument containing whatever value it returned the last time it ran. On the initial run, this is `None`.
By default, effects **do not run on the server**. This means you can call browser-specific APIs within the effect function without causing issues. If you need an effect to run on the server, use [`create_isomorphic_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_isomorphic_effect.html).
## Autotracking and Dynamic Dependencies
If youre familiar with a framework like React, you might notice one key difference. React and similar frameworks typically require you to pass a “dependency array,” an explicit set of variables that determine when the effect should rerun.
Because Leptos comes from the tradition of synchronous reactive programming, we dont need this explicit dependency list. Instead, we automatically track dependencies depending on which signals are accessed within the effect.
This has two effects (no pun intended). Dependencies are:
1. **Automatic**: You dont need to maintain a dependency list, or worry about what should or shouldnt be included. The framework simply tracks which signals might cause the effect to rerun, and handles it for you.
2. **Dynamic**: The dependency list is cleared and updated every time the effect runs. If your effect contains a conditional (for example), only signals that are used in the current branch are tracked. This means that effects rerun the absolute minimum number of times.
> If this sounds like magic, and if you want a deep dive into how automatic dependency tracking works, [check out this video](https://www.youtube.com/watch?v=GWB3vTWeLd4). (Apologies for the low volume!)
## Effects as Zero-Cost-ish Abstraction
While theyre not a “zero-cost abstraction” in the most technical sense—they require some additional memory use, exist at runtime, etc.—at a higher level, from the perspective of whatever expensive API calls or other work youre doing within them, effects are a zero-cost abstraction. They rerun the absolute minimum number of times necessary, given how youve described them.
Imagine that Im creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
```rust
let (first, set_first) = create_signal(cx, String::new());
let (last, set_last) = create_signal(cx, String::new());
let (use_last, set_use_last) = create_signal(cx, true);
// this will add the name to the log
// any time one of the source signals changes
create_effect(cx, move |_| {
log(
cx,
if use_last() {
format!("{} {}", first(), last())
} else {
first()
},
)
});
```
If `use_last` is `true`, effect should rerun whenever `first`, `last`, or `use_last` changes. But if I toggle `use_last` to `false`, a change in `last` will never cause the full name to change. In fact, `last` will be removed from the dependency list until `use_last` toggles again. This saves us from sending multiple unnecessary requests to the API if I change `last` multiple times while `use_last` is still `false`.
## To `create_effect`, or not to `create_effect`?
Effects are intended to run _side-effects_ of the system, not to synchronize state _within_ the system. In other words: dont write to signals within effects.
If you need to define a signal that depends on the value of other signals, use a derived signal or [`create_memo`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_memo.html).
If you need to synchronize some reactive value with the non-reactive world outside—like a web API, the console, the filesystem, or the DOM—create an effect.
> If youre curious for more information about when you should and shouldnt use `create_effect`, [check out this video](https://www.youtube.com/watch?v=aQOFJQ2JkvQ) for a more in-depth consideration!
## Effects and Rendering
Weve managed to get this far without mentioning effects because theyre built into the Leptos DOM renderer. Weve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
```rust
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<p>{count}</p>
}
```
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
```rust
let (count, set_count) = create_signal(cx, 0);
// create a DOM element
let p = create_element("p");
// create an effect to reactively update the text
create_effect(cx, move |prev_value| {
// first, access the signals value and convert it to a string
let text = count().to_string();
// if this is different from the previous value, update the node
if prev_value != Some(text) {
p.set_text_content(&text);
}
// return this value so we can memoize the next update
text
});
```
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,5 @@
# Reactivity
Leptos is built on top of a fine-grained reactive system, designed to run expensive side effects (like rendering something in a browser, or making a network request) as infrequently as possible in response to change, reactive values.
So far weve seen signals in action. These chapters will go into a bit more depth, and look at effects, which are the other half of the story.

View File

@@ -0,0 +1,106 @@
# Working with Signals
So far weve used some simple examples of [`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html), which returns a [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) getter and a [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) setter.
## Getting and Setting
There are four basic signal operations:
1. [`.get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) clones the current value of the signal and tracks any future changes to the value reactively.
2. [`.with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalWith%3CT%3E-for-ReadSignal%3CT%3E) takes a function, which receives the current value of the signal by reference (`&T`), and tracks any future changes.
3. [`.set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalSet%3CT%3E-for-WriteSignal%3CT%3E) replaces the current value of the signal and notifies any subscribers that they need to update.
4. [`.update()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalUpdate%3CT%3E-for-WriteSignal%3CT%3E) takes a function, which receives a mutable reference to the current value of the signal (`&mut T`), and notifies any subscribers that they need to update. (`.update()` doesnt return the value returned by the closure, but you can use [`.try_update()`](https://docs.rs/leptos/latest/leptos/trait.SignalUpdate.html#tymethod.try_update) if you need to; for example, if youre removing an item from a `Vec<_>` and want the removed item.)
Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `WriteSignal` as a function is syntax sugar for `.set()`. So
```rust
let (count, set_count) = create_signal(cx, 0);
set_count(1);
log!(count());
```
is the same as
```rust
let (count, set_count) = create_signal(cx, 0);
set_count.set(1);
log!(count.get());
```
You might notice that `.get()` and `.set()` can be implemented in terms of `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)`.
But of course, `.get()` and `.set()` (or the plain function-call forms!) are much nicer syntax.
However, there are some very good use cases for `.with()` and `.update()`.
For example, consider a signal that holds a `Vec<String>`.
```rust
let (names, set_names) = create_signal(cx, Vec::new());
if names().is_empty() {
set_names(vec!["Alice".to_string()]);
}
```
In terms of logic, this is simple enough, but its hiding some significant inefficiencies. Remember that `names().is_empty()` is sugar for `names.get().is_empty()`, which clones the value (its `names.with(|n| n.clone()).is_empty()`). This means we clone the whole `Vec<String>`, run `is_empty()`, and then immediately throw away the clone.
Likewise, `set_names` replaces the value with a whole new `Vec<_>`. This is fine, but we might as well just mutate the original `Vec<_>` in place.
```rust
let (names, set_names) = create_signal(cx, Vec::new());
if names.with(|names| names.is_empty()) {
set_names.update(|names| names.push("Alice".to_string()));
}
```
Now our function simply takes `names` by reference to run `is_empty()`, avoiding that clone.
And if you have Clippy on, or if you have sharp eyes, you may notice we can make this even neater:
```rust
if names.with(Vec::is_empty) {
// ...
}
```
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unncessary closure.
## Making signals depend on each other
Often people ask about situations in which some signal needs to change based on some other signals value. There are three good ways to do this, and one thats less than ideal but okay under controlled circumstances.
### Good Options
**1) B is a function of A.** Create a signal for A and a derived signal or memo for B.
```rust
let (count, set_count) = create_signal(cx, 1);
let derived_signal_double_count = move || count() * 2;
let memoized_double_count = create_memo(cx, move |_| count() * 2);
```
> For guidance on whether to use a derived signal or a memo, see the docs for [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html)
>
**2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
```rust
let (first_name, set_first_name) = create_signal(cx, "Bridget".to_string());
let (last_name, set_last_name) = create_signal(cx, "Jones".to_string());
let full_name = move || format!("{} {}", first_name(), last_name());
```
**3) A and B are independent signals, but sometimes updated at the same time.** When you make the call to update A, make a separate call to update B.
```rust
let (age, set_age) = create_signal(cx, 32);
let (favorite_number, set_favorite_number) = create_signal(cx, 42);
// use this to handle a click on a `Clear` button
let clear_handler = move |_| {
set_age(0);
set_favorite_number(0);
};
```
### If you really must...
**4) Create an effect to write to B whenever A changes.** This is officially discouraged, for several reasons:
a) It will always be less efficient, as it means every time A updates you do two full trips through the reactive process. (You set A, which causes the effect to run, as well as any other effects that depend on A. Then you set B, which causes any effects that depend on B to run.)
b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals from effects.
In most situations, its best to rewrite things such that theres a clear, top-down data flow based on derived signals or memos. But this isnt the end of the world.
> Im intentionally not providing an example here. Read the [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) docs to figure out how this would work.

View File

@@ -34,7 +34,7 @@ use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! {
view! { cx,
<Router>
<nav>
/* ... */
@@ -59,7 +59,7 @@ use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! {
view! { cx,
<Router>
<nav>
/* ... */

View File

@@ -39,7 +39,7 @@ struct ContactSearch {
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
The typed versions return `Memo<Result<T>, _>`. Its a Memo so it reacts to changes in the URL. Its a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
The typed versions return `Memo<Result<T, _>>`. Its a Memo so it reacts to changes in the URL. Its a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
```rust
let params = use_params::<ContactParams>(cx);

View File

@@ -4,10 +4,10 @@
Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?”
A URL consists of many parts. For example, the URL `https://leptos.dev/blog/search?q=Search#results` consists of
A URL consists of many parts. For example, the URL `https://my-cool-blog.com/blog/search?q=Search#results` consists of
- a _scheme_: `https`
- a _domain_: `leptos.dev`
- a _domain_: `my-cool-blog.com`
- a **path**: `/blog/search`
- a **query** (or **search**): `?q=Search`
- a _hash_: `#results`

View File

@@ -0,0 +1,124 @@
# 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>
}
}
```
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 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!
> **Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?**
>
> These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the JSON format.
>
> The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, HTML forms dont support `PUT` or `DELETE`, and they dont support sending JSON. This means that if you use anything but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. As well see [in a later chapter](../progressive_enhancement), this isnt always a great idea.
>
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didnt support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
## 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,73 @@
# Extractors
The server functions we looked at in the last chapter showed how to run code on the server, and integrate it with the user interface youre rendering in the browser. But they didnt show you much about how to actually use your server to its full potential.
## Server Frameworks
We call Leptos a “full-stack” framework, but “full-stack” is always a misnomer (after all, it never means everything from the browser to your power company.) For us, “full stack” means that your Leptos app can run in the browser, and can run on the server, and can integrate the two, drawing together the unique features available in each; as weve seen in the book so far, a button click on the browser can drive a database read on the server, both written in the same Rust module. But Leptos itself doesnt provide the server (or the database, or the operating system, or the firmware, or the electrical cables...)
Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_actix/latest/leptos_axum/)). Weve built integrations with each servers router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
> If havent seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, nows a good time to check them out.
## Using Extractors
Both Actix and Axum handlers are built on the same powerful idea of **extractors**. Extractors “extract” typed data from an HTTP request, allowing you to access server-specific data easily.
Leptos provides `extract` helper functions to let you use these extractors directly in your server functions, with a convenient syntax very similar to handlers for each framework.
### Actix Extractors
The [`extract` function in `leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/fn.extract.html) takes a handler function as its argument. The handler follows similar rules to an Actix handler: it is an async function that receives arguments that will be extracted from the request and returns some value. The handler function receives that extracted data as its arguments, and can do further `async` work on them inside the body of the `async move` block. It returns whatever value you return back out into the server function.
```rust
#[server(ActixExtract, "/api")]
pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
use leptos_actix::extract;
use actix_web::dev::ConnectionInfo;
use actix_web::web::{Data, Query};
extract(cx,
|search: Query<Search>, connection: ConnectionInfo| async move {
format!(
"search = {}\nconnection = {:?}",
search.q,
connection
)
},
)
.await
}
```
## Axum Extractors
The syntax for the `leptos_axum::extract` function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so youll need to add something to handle the error case.
```rust
#[server(AxumExtract, "/api")]
pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
use axum::{extract::Query, http::Method};
use leptos_axum::extract;
extract(cx, |method: Method, res: Query<MyQuery>| async move {
format!("{method:?} and {}", res.q)
},
)
.await
.map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string()))
}
```
These are relatively simple examples accessing basic data from the server. But you can use extractors to access things like headers, cookies, database connection pools, and more, using the exact same `extract()` pattern.
> Note: For now, the Axum `extract` function only supports extractors for which the state is `()`, i.e., you can't yet use it to extract `State(_)`. You can access `State(_)` by using a custom handler that extracts the state and then provides it via context. [Click here for an example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/main.rs#L91-L92).
## A Note about Data-Loading Patterns
Because Actix and (especially) Axum are built on the idea of a single round-trip HTTP request and response, you typically run extractors near the “top” of your application (i.e., before you start rendering) and use the extracted data to determine how that should be rendered. Before you render a `<button>`, you load all the data your app could need. And any given route handler needs to know all the data that will need to be extracted by that route.
But Leptos integrates both the client and the server, and its important to be able to refresh small pieces of your UI with new data from the server without forcing a full reload of all the data. So Leptos likes to push data loading “down” in your application, as far towards the leaves of your user interface as possible. When you click a `<button>`, it can refresh just the data it needs. This is exactly what server functions are for: they give you granular access to data to be loaded and reloaded.
The `extract()` functions let you combine both models by using extractors in your server functions. You get access to the full power of route extractors, while decentralizing knowledge of what needs to be extracted down to your individual components. This makes it easier to refactor and reorganize routes: you dont need to specify all the data a route needs up front.

View File

@@ -0,0 +1,74 @@
# Responses and Redirects
Extractors provide an easy way to access request data inside server functions. Leptos also provides a way to modify the HTTP response, using the `ResponseOptions` type (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/struct.ResponseOptions.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/struct.ResponseOptions.html)) types and the `redirect` helper function (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/fn.redirect.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/fn.redirect.html)).
## `ResponseOptions`
`ResponseOptions` is provided via context during the initial server rendering response and during any subsequent server function call. It allows you to easily set the status code for the HTTP response, or to add headers to the HTTP response, e.g., to set cookies.
```rust
#[server(TeaAndCookies)]
pub async fn tea_and_cookies(cx: Scope) -> Result<(), ServerFnError> {
use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue};
use leptos_actix::ResponseOptions;
// pull ResponseOptions from context
let response = expect_context::<ResponseOptions>(cx);
// set the HTTP status code
response.set_status(StatusCode::IM_A_TEAPOT);
// set a cookie in the HTTP response
let mut cookie = Cookie::build("biscuits", "yes").finish();
if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) {
res.insert_header(header::SET_COOKIE, cookie);
}
}
```
## `redirect`
One common modification to an HTTP response is to redirect to another page. The Actix and Axum integrations provide a `redirect` function to make this easy to do. `redirect` simply sets an HTTP status code of `302 Found` and sets the `Location` header.
Heres a simplified example from our [`session_auth_axum` example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/auth.rs#L154-L181).
```rust
#[server(Login, "/api")]
pub async fn login(
cx: Scope,
username: String,
password: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
// pull the DB pool and auth provider from context
let pool = pool(cx)?;
let auth = auth(cx)?;
// check whether the user exists
let user: User = User::get_from_username(username, &pool)
.await
.ok_or_else(|| {
ServerFnError::ServerError("User does not exist.".into())
})?;
// check whether the user has provided the correct password
match verify(password, &user.password)? {
// if the password is correct...
true => {
// log the user in
auth.login_user(user.id);
auth.remember_user(remember.is_some());
// and redirect to the home page
leptos_axum::redirect(cx, "/");
Ok(())
}
// if not, return an error
false => Err(ServerFnError::ServerError(
"Password does not match.".to_string(),
)),
}
}
```
This server function can then be used from your application. This `redirect` works well with the progressively-enhanced `<ActionForm/>` component: without JS/WASM, the server response will redirect because of the status code and header. With JS/WASM, the `<ActionForm/>` will detect the redirect in the server function response, and use client-side navigation to redirect to the new page.

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

@@ -32,6 +32,6 @@ cargo leptos watch
Once your app has compiled you can open up your browser to [`http://localhost:3000`](http://localhost:3000) to see it.
`cargo-leptos` has lots of additional features and built in tools. You can learn more [in its `README`](https://github.com/leptos-rs/leptos/blob/main/examples/hackernews/src/api.rs).
`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

@@ -62,6 +62,16 @@ If youre using server-side rendering, the synchronous mode is almost never wh
- Able to show the fallback loading state and dynamically replace it, instead of showing blank sections for un-loaded data.
- _Cons_: Requires JavaScript to be enabled for suspended fragments to appear in correct order. (This small chunk of JS streamed down in a `<script>` tag alongside the `<template>` tag that contains the rendered `<Suspense/>` fragment, so it does not need to load any additional JS files.)
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. If one of them reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order as usual.
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is *not* useful if theres only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
- _Pros_: Works if JavaScript is disabled or not supported on the users device.
- _Cons_
- Slower initial response time than out-of-order.
- Marginally overall response due to additional work on the server.
- No fallback state shown.
## Using SSR Modes
Because it offers the best blend of performance characteristics, Leptos defaults to out-of-order streaming. But its really simple to opt into these different modes. You do it by adding an `ssr` property onto one or more of your `<Route/>` components, like in the [`ssr_modes` example](https://github.com/leptos-rs/leptos/blob/main/examples/ssr_modes/src/app.rs).

View File

@@ -28,7 +28,7 @@ fn App(cx: Scope) -> impl IntoView {
view! { cx,
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
set_count(3);
}
>
"Click me: "
@@ -78,8 +78,7 @@ This returns a `(getter, setter)` tuple. To access the current value, youll
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
current value, youll call `set_count.set(...)` (or `set_count(...)`).
> `.get()` clones the value and `.set()` overwrites it. In many cases, its more
> efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if youd like to learn more about those trade-offs at this point.
> `.get()` clones the value and `.set()` overwrites it. In many cases, its more efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if youd like to learn more about those trade-offs at this point.
## The View
@@ -90,7 +89,8 @@ view! { cx,
<button
// define an event listener with on:
on:click=move |_| {
set_count.update(|n| *n += 1);
// on stable, this is set_count.set(3);
set_count(3);
}
>
// text nodes are wrapped in quotation marks
@@ -142,6 +142,16 @@ in a function, telling the framework to update the view every time `count` chang
`{count()}` access the value of `count` once, and passes an `i32` into the view,
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
Lets make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Lets replace “set this value to 3” with “increment this value by 1”:
```rust
move |_| {
set_count.update(|n| *n += 1);
}
```
You can see here that while `set_count` just sets the value, `set_count.update()` gives us a mutable reference and mutates the value in place. Either one will trigger a reactive update in our UI.
> Throughout this tutorial, well use CodeSandbox to show interactive examples. To
> show the browser in the sandbox, you may need to click `Add DevTools >
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details

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,12 +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>
> <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

@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
view! {
view! { cx,
<progress
max="50"
value=count
@@ -98,18 +98,6 @@ notice that you can easily tell the difference between an element and a componen
because components always have `PascalCase` names. You pass the `progress` prop
in as if it were an HTML element attribute. Simple.
> ### Important Note
>
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
> is what allows us to have named props, when Rust does not have named function parameters.
> If youre defining a component in one module and importing it into another, make
> sure you include this `ComponentProps` type:
>
> `use progress_bar::{ProgressBar, ProgressBarProps};`
>
> **Note**: This is still true as of `0.2.5`, but the requirement has been removed on `main`
> and will not apply to later versions.
### Reactive and Static Props
Youll notice that throughout this example, `progress` takes a reactive

View File

@@ -198,7 +198,7 @@ let (value, set_value) = create_signal(cx, 0);
view! { cx,
<Show
when=move || value() > 5
when=move || { value() > 5 }
fallback=|cx| view! { cx, <Small/> }
>
<Big/>

View File

@@ -19,7 +19,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
view! { cx,
<label>
"Type a number (or not!)"
<input type="number" on:input=on_input/>
<input on:input=on_input/>
<p>
"You entered "
<strong>{value}</strong>
@@ -69,7 +69,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input=on_input/>
<input on:input=on_input/>
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,

View File

@@ -117,7 +117,7 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ButtonC<F>(cx: Scope) -> impl IntoView {
pub fn ButtonC(cx: Scope) -> impl IntoView {
view! { cx,
<button>"Toggle"</button>
}

View File

@@ -1,4 +1,4 @@
extend = [{ path = "./cargo-make/common.toml" }]
extend = [{ path = "./cargo-make/main.toml" }]
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
@@ -15,42 +15,33 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"fetch",
"hackernews",
"hackernews_axum",
"js-framework-benchmark",
"leptos-tailwind-axum",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"slots",
"ssr_modes",
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
[tasks.verify]
description = "Run all quality checks and tests"
dependencies = ["check-style", "test-unit-and-web"]
[tasks.test-unit-and-web]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.pre-verify-flow]
[tasks.post-verify-flow]
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
[tasks.pre-web-test-flow]
[tasks.web-test]
[tasks.post-web-test-flow]
[tasks.gen-members]
workspace = false
description = "Generate the list of workspace members"
script = '''
examples=$(ls |
grep -v README.md |
grep -v Makefile.toml |
grep -v cargo-make |
grep -v gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
'''

7
examples/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Examples
The examples in this directory are all built and tested against the current `main` branch.
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).

View File

@@ -0,0 +1,5 @@
[tasks.test-e2e]
dependencies = ["setup-node", "cargo-leptos-e2e"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]

View File

@@ -1,14 +1,98 @@
[env]
CARGO_MAKE_CLIPPY_ARGS = "--all-targets -- -D warnings"
[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.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
[tasks.test-local]
description = "Run all tests from an example directory"
dependencies = ["test", "web-test"]
[tasks.clean-cargo]
description = "Runs the cargo clean command."
category = "Cleanup"
command = "cargo"
args = ["clean"]
[tasks.clean-trunk]
description = "Runs the trunk clean command."
category = "Cleanup"
command = "trunk"
args = ["clean"]
[tasks.clean-node_modules]
description = "Delete all node_modules directories"
category = "Cleanup"
script = '''
find . -type d -name node_modules | xargs rm -rf
'''
[tasks.clean-playwright]
description = "Delete playwright directories"
category = "Cleanup"
script = '''
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
do
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
done
'''
[tasks.clean-all]
description = "Delete all temporary directories"
category = "Cleanup"
dependencies = ["clean-cargo"]
[tasks.test-wasm]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.cargo-leptos-e2e]
description = "Runs end to end tests with cargo leptos"
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.setup-node]
description = "Install node dependencies and playwright browsers"
env = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1" }
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
# Discover commands
if command -v pnpm; then
NODE_CMD=pnpm
PLAYWRIGHT_CMD=pnpm
elif command -v npm; then
NODE_CMD=npm
PLAYWRIGHT_CMD=npx
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
# Install node dependencies
for node_path in $(find . -name package.json -not -path '*/node_modules/*')
do
node_dir=$(dirname $node_path)
echo Install node dependencies for $node_dir
cd $node_dir
${NODE_CMD} install
cd ${project_dir}
done
# Install playwright browsers
for pw_path in $(find . -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
echo Install playwright browsers for $pw_dir
cd $pw_dir
${PLAYWRIGHT_CMD} playwright install
cd $project_dir
done
'''

View File

@@ -0,0 +1,35 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.ci]
alias = "verify-flow"
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify", "verify", "post-verify"]
[tasks.verify]
description = "Run all quality checks and tests"
dependencies = ["check-style", "test-unit-and-e2e"]
[tasks.test-unit-and-e2e]
description = "Run all unit and e2e tests"
dependencies = ["test-flow", "test-e2e-flow"]
[tasks.pre-verify]
[tasks.post-verify]
dependencies = ["maybe-clean-all"]
[tasks.maybe-clean-all]
description = "Used to clean up locally after call to verify-examples"
condition = { env_true = ["CLEAN_AFTER_VERIFY"] }
[tasks.test-e2e-flow]
description = "Provides pre and post hooks for test-e2e"
dependencies = ["pre-test-e2e", "test-e2e", "post-test-e2e"]
[tasks.pre-test-e2e]
[tasks.test-e2e]
[tasks.post-test-e2e]

View File

@@ -0,0 +1,7 @@
extend = [{ path = "../cargo-make/playwright.toml" }]
[tasks.test-e2e]
dependencies = ["setup-node", "test-playwright-autostart"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]

View File

@@ -0,0 +1,119 @@
[tasks.clean-playwright]
description = "Delete playwright directories"
category = "Cleanup"
script = '''
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
do
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
done
'''
[tasks.test-playwright-autostart]
description = "Run playwright test with server autostart"
category = "Test"
command = "npm"
args = ["run", "e2e:auto-start"]
[tasks.test-playwright]
description = "Run playwright test"
category = "Test"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
# Discover commands
if command -v pnpm; then
PLAYWRIGHT_CMD=pnpm
elif command -v npm; then
PLAYWRIGHT_CMD=npx
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
# Run playwright command
for pw_path in $(find . -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
cd $pw_dir
${PLAYWRIGHT_CMD} playwright test
cd $project_dir
done
'''
[tasks.test-playwright-ui]
description = "Run playwright test --ui"
category = "Test"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
# Discover commands
if command -v pnpm; then
PLAYWRIGHT_CMD=pnpm
elif command -v npm; then
PLAYWRIGHT_CMD=npx
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
# Run playwright command
for pw_path in $(find . -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
cd $pw_dir
${PLAYWRIGHT_CMD} playwright test --ui
cd $project_dir
done
'''
[tasks.test-playwright-report]
description = "Run playwright show-report"
category = "Test"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
# Discover commands
if command -v pnpm; then
PLAYWRIGHT_CMD=pnpm
elif command -v npm; then
PLAYWRIGHT_CMD=npx
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
# Run playwright command
for pw_path in $(find . -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
cd $pw_dir
${PLAYWRIGHT_CMD} playwright show-report
cd $project_dir
done
'''
# ALIASES
[tasks.pw]
dependencies = ["test-playwright"]
[tasks.pw-ui]
dependencies = ["test-playwright-ui"]
[tasks.pw-report]
dependencies = ["test-playwright-report"]

View File

@@ -0,0 +1,22 @@
[tasks.build]
command = "trunk"
args = ["build"]
[tasks.clean-trunk]
command = "trunk"
args = ["clean"]
[tasks.start-trunk]
command = "trunk"
args = ["serve", "--open"]
[tasks.stop-trunk]
script = '''
pkill -f "cargo-make"
pkill -f "trunk"
'''
# ALIASES
[tasks.dev]
dependencies = ["start-trunk"]

View File

@@ -0,0 +1,9 @@
[tasks.test]
env = { RUN_CARGO_TEST = false }
condition = { env_true = ["RUN_CARGO_TEST"] }
[tasks.post-test]
dependencies = ["test-wasm"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-trunk"]

View File

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

View File

@@ -8,7 +8,7 @@ codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -1,6 +1,6 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
]
[tasks.build]

View File

@@ -19,19 +19,17 @@ console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
log = "0.4"
gloo-net = { git = "https://github.com/rustwasm/gloo" }
wasm-bindgen = "0.2"
wasm-bindgen = "=0.2.87"
serde = { version = "1", features = ["derive"] }
[features]
default = []
default = ["nightly"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
@@ -41,10 +39,10 @@ ssr = [
"leptos_meta/ssr",
"leptos_router/ssr",
]
stable = ["leptos/stable", "leptos_router/stable"]
nightly = ["leptos/nightly", "leptos_router/nightly"]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix", "stable"]
denylist = ["actix-files", "actix-web", "leptos_actix", "nightly"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]

View File

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

View File

@@ -1,27 +1,20 @@
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;
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
_ = GetServerCount::register();
_ = AdjustServerCount::register();
_ = ClearServerCount::register();
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
}
}
#[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 +22,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 +42,35 @@ 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=Counter/>
<Route path="form" view=FormCounter/>
<Route path="multi" view=MultiuserCounter/>
<Route path="multi" view=NotFound/>
</Routes>
</main>
</Router>
@@ -93,33 +88,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,27 +151,19 @@ 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
<ActionForm action=clear>
<input type="submit" value="Clear"/>
</ActionForm>
// We can submit named arguments to the server functions
// by including them as input values with the same name
<ActionForm action=adjust>
<input type="hidden" name="delta" value="-1"/>
<input type="hidden" name="msg" value="form value down"/>
@@ -185,26 +186,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,20 +219,33 @@ 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>
}
}
#[component]
fn NotFound(cx: Scope) -> impl IntoView {
#[cfg(feature = "ssr")]
{
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
}
view! { cx, <h1>"Not Found"</h1> }
}

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::*;
@@ -31,13 +31,18 @@ cfg_if! {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
crate::counters::register_server_functions();
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines
// below to register the server functions.
// _ = GetServerCount::register();
// _ = AdjustServerCount::register();
// _ = ClearServerCount::register();
// Setting this to None means we'll be using cargo-leptos and its env vars.
// 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 || {
@@ -47,15 +52,36 @@ cfg_if! {
App::new()
.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))
// serve JS/WASM/CSS from `pkg`
.service(Files::new("/pkg", format!("{site_root}/pkg")))
// serve other assets from the `assets` directory
.service(Files::new("/assets", site_root))
// serve the favicon from /favicon.ico
.service(favicon)
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
Counters,
)
.app_data(web::Data::new(leptos_options.to_owned()))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
#[actix_web::get("favicon.ico")]
async fn favicon(
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
) -> actix_web::Result<actix_files::NamedFile> {
let leptos_options = leptos_options.into_inner();
let site_root = &leptos_options.site_root;
Ok(actix_files::NamedFile::open(format!(
"{site_root}/favicon.ico"
))?)
}
}
// client-only main for Trunk
else {

View File

@@ -8,7 +8,7 @@ codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
leptos = { path = "../../leptos", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -1,6 +1,6 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
]
[tasks.build]

View File

@@ -1,6 +1,7 @@
use counter_without_macros::counter;
use leptos::*;
/// Show the counter
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"

View File

@@ -1,6 +1,6 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
]
[tasks.build]

View File

@@ -1,4 +1,4 @@
use leptos::{For, ForProps, *};
use leptos::{For, *};
const MANY_COUNTERS: usize = 1000;

View File

@@ -107,7 +107,7 @@ fn inc() {
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> \
<Each> --><!-- <EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->2<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \

20
examples/counters_stable/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# Support playwright testing
node_modules/
test-results/
end2end/playwright-report/
playwright/.cache/
pnpm-lock.yaml
# Support trunk
dist

View File

@@ -4,11 +4,24 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
leptos = { path = "../../leptos", features = ["csr"] }
leptos_meta = { path = "../../meta", features = ["csr"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2.87"
wasm-bindgen-test = "0.3.37"
pretty_assertions = "1.3.0"
[dev-dependencies.web-sys]
features = [
"Event",
"EventInit",
"EventTarget",
"HtmlElement",
"HtmlInputElement",
"XPathResult",
]
version = "0.3.64"

View File

@@ -1,3 +1,10 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]
[tasks.build]
command = "cargo"
args = ["+stable", "build-all-features"]

View File

@@ -0,0 +1,4 @@
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

View File

@@ -0,0 +1,83 @@
{
"name": "grip",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "grip",
"devDependencies": {
"@playwright/test": "^1.35.1"
}
},
"node_modules/.pnpm/@playwright+test@1.33.0": {
"extraneous": true
},
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
"version": "20.2.1",
"extraneous": true,
"license": "MIT"
},
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
"version": "1.33.0",
"extraneous": true,
"license": "Apache-2.0",
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@playwright/test": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.35.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/@types/node": {
"version": "20.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright-core": {
"version": "1.35.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
}
}
}

View File

@@ -0,0 +1,7 @@
{
"private": "true",
"scripts": {},
"devDependencies": {
"@playwright/test": "^1.35.1"
}
}

View File

@@ -0,0 +1,77 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !process.env.DEV,
/* Retry on CI only */
retries: process.env.DEV ? 0 : 10,
/* Opt out of parallel tests on CI. */
workers: process.env.DEV ? 1 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [["html", { open: "never" }], ["list"]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:8080",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: "firefox",
// use: { ...devices["Desktop Firefox"] },
// },
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: "cd ../ && trunk serve",
// url: "http://127.0.0.1:8080",
// reuseExistingServer: false, //!process.env.CI,
// },
});

View File

@@ -0,0 +1,19 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add 1000 Counters", () => {
test("should increase the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await Promise.all([
await ui.goto(),
await ui.addOneThousandCountersButton.waitFor(),
]);
await ui.addOneThousandCounters();
await ui.addOneThousandCounters();
await ui.addOneThousandCounters();
await expect(ui.counters).toHaveText("3000");
});
});

View File

@@ -0,0 +1,15 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Add Counter", () => {
test("should increase the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await expect(ui.counters).toHaveText("3");
});
});

View File

@@ -0,0 +1,18 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Clear Counters", () => {
test("should reset the counts", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.clearCounters();
await expect(ui.total).toHaveText("0");
await expect(ui.counters).toHaveText("0");
});
});

View File

@@ -0,0 +1,16 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Decrement Count", () => {
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.decrementCount();
await ui.decrementCount();
await ui.decrementCount();
await expect(ui.total).toHaveText("-3");
});
});

View File

@@ -0,0 +1,30 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Enter Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.enterCount("5");
await expect(ui.total).toHaveText("5");
await expect(ui.counters).toHaveText("1");
});
test("should decrease the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.enterCount("100");
await ui.enterCount("100", 1);
await ui.enterCount("100", 2);
await ui.enterCount("50", 1);
await expect(ui.total).toHaveText("250");
});
});

View File

@@ -0,0 +1,98 @@
import { expect, Locator, Page } from "@playwright/test";
export class CountersPage {
readonly page: Page;
readonly addCounterButton: Locator;
readonly addOneThousandCountersButton: Locator;
readonly clearCountersButton: Locator;
readonly incrementCountButton: Locator;
readonly counterInput: Locator;
readonly decrementCountButton: Locator;
readonly removeCountButton: Locator;
readonly total: Locator;
readonly counters: Locator;
constructor(page: Page) {
this.page = page;
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
this.addOneThousandCountersButton = page.locator("button", {
hasText: "Add 1000 Counters",
});
this.clearCountersButton = page.locator("button", {
hasText: "Clear Counters",
});
this.decrementCountButton = page.locator("button", {
hasText: "-1",
});
this.incrementCountButton = page.locator("button", {
hasText: "+1",
});
this.removeCountButton = page.locator("button", {
hasText: "x",
});
this.total = page.getByTestId("total");
this.counters = page.getByTestId("counters");
this.counterInput = page.getByRole("textbox");
}
async goto() {
await this.page.goto("/");
}
async addCounter() {
await Promise.all([
this.addCounterButton.waitFor(),
this.addCounterButton.click(),
]);
}
async addOneThousandCounters() {
this.addOneThousandCountersButton.click();
}
async decrementCount(index: number = 0) {
await Promise.all([
this.decrementCountButton.nth(index).waitFor(),
this.decrementCountButton.nth(index).click(),
]);
}
async incrementCount(index: number = 0) {
await Promise.all([
this.incrementCountButton.nth(index).waitFor(),
this.incrementCountButton.nth(index).click(),
]);
}
async clearCounters() {
await Promise.all([
this.clearCountersButton.waitFor(),
this.clearCountersButton.click(),
]);
}
async enterCount(count: string, index: number = 0) {
await Promise.all([
this.counterInput.nth(index).waitFor(),
this.counterInput.nth(index).fill(count),
]);
}
async removeCounter(index: number = 0) {
await Promise.all([
this.removeCountButton.nth(index).waitFor(),
this.removeCountButton.nth(index).click(),
]);
}
}

View File

@@ -0,0 +1,16 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Increment Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.incrementCount();
await ui.incrementCount();
await ui.incrementCount();
await expect(ui.total).toHaveText("3");
});
});

View File

@@ -0,0 +1,17 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Remove Counter", () => {
test("should decrement the number of counters", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.addCounter();
await ui.addCounter();
await ui.removeCounter(1);
await expect(ui.counters).toHaveText("2");
});
});

View File

@@ -0,0 +1,19 @@
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("View Counters", () => {
test("should see the title", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await expect(page).toHaveTitle("Counters (Stable)");
});
test("should see the initial counts", async ({ page }) => {
const counters = new CountersPage(page);
await counters.goto();
await expect(counters.total).toHaveText("0");
await expect(counters.counters).toHaveText("0");
});
});

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Counters (Stable)</title>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
</head>
<body></body>

View File

@@ -0,0 +1,11 @@
{
"private": "true",
"scripts": {
"start-server": "trunk serve",
"e2e": "cargo make test-playwright",
"e2e:auto-start": "start-server-and-test start-server http://127.0.0.1:8080 e2e"
},
"devDependencies": {
"start-server-and-test": "^1.15.4"
}
}

View File

@@ -0,0 +1,109 @@
use leptos::*;
use leptos_meta::*;
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
provide_context(cx, CounterUpdater { set_counters });
provide_meta_context(cx);
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(cx, 0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(cx, 0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! { cx,
<Title text="Counters (Stable)" />
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |cx, (id, (value, set_value))| {
view! {
cx,
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
cx: Scope,
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! { cx,
<li>
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input data-testid="counter_input" type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,3 +1,4 @@
use counters_stable::Counters;
use leptos::*;
fn main() {
@@ -5,108 +6,3 @@ fn main() {
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counters/> })
}
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
provide_context(cx, CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(cx, 0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(cx, 0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! { cx,
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span>{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span>{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |cx, (id, (value, set_value))| {
view! {
cx,
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
cx: Scope,
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! { cx,
<li>
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{move || value.get().to_string()}</span>
<button on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -0,0 +1,17 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_1k_counters();
ui::add_1k_counters();
ui::add_1k_counters();
// Then
assert_eq!(ui::counters(), 3000);
}

View File

@@ -0,0 +1,17 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_counter();
ui::add_counter();
ui::add_counter();
// Then
assert_eq!(ui::counters(), 3);
}

View File

@@ -0,0 +1,19 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_reset_the_counts() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::clear_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}

View File

@@ -0,0 +1,18 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrease_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::decrement_counter(1);
ui::decrement_counter(1);
ui::decrement_counter(1);
// Then
assert_eq!(ui::total(), -3);
}

View File

@@ -0,0 +1,34 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::enter_count(1, 5);
// Then
assert_eq!(ui::total(), 5);
}
#[wasm_bindgen_test]
fn should_decrease_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::enter_count(1, 100);
ui::enter_count(2, 100);
ui::enter_count(3, 100);
ui::enter_count(1, 50);
// Then
assert_eq!(ui::total(), 250);
}

View File

@@ -0,0 +1,112 @@
use counters_stable::Counters;
use leptos::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
// Actions
pub fn add_1k_counters() {
find_by_text("Add 1000 Counters").click();
}
pub fn add_counter() {
find_by_text("Add Counter").click();
}
pub fn clear_counters() {
find_by_text("Clear Counters").click();
}
pub fn decrement_counter(index: u32) {
counter_html_element(index, "decrement_count").click();
}
pub fn enter_count(index: u32, count: i32) {
let input = counter_input_element(index, "counter_input");
input.set_value(count.to_string().as_str());
let mut event_init = EventInit::new();
event_init.bubbles(true);
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
input.dispatch_event(&event).unwrap();
}
pub fn increment_counter(index: u32) {
counter_html_element(index, "increment_count").click();
}
pub fn remove_counter(index: u32) {
counter_html_element(index, "remove_counter").click();
}
pub fn view_counters() {
remove_existing_counters();
mount_to_body(|cx| view! { cx, <Counters/> });
}
// Results
pub fn counters() -> i32 {
data_test_id("counters").parse::<i32>().unwrap()
}
pub fn title() -> String {
leptos::document().title()
}
pub fn total() -> i32 {
data_test_id("total").parse::<i32>().unwrap()
}
// Internal
fn counter_element(index: u32, text: &str) -> Element {
let selector =
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
leptos::document()
.query_selector(&selector)
.unwrap()
.unwrap()
}
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
counter_element(index, text)
.dyn_into::<HtmlElement>()
.unwrap()
}
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
counter_element(index, text)
.dyn_into::<HtmlInputElement>()
.unwrap()
}
fn data_test_id(id: &str) -> String {
let selector = format!("[data-testid=\"{}\"]", id);
leptos::document()
.query_selector(&selector)
.unwrap()
.expect("counters not found")
.text_content()
.unwrap()
}
fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = leptos::document();
document
.evaluate(&xpath, &document)
.unwrap()
.iterate_next()
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
}
fn remove_existing_counters() {
if let Some(counter) =
leptos::document().query_selector("body div").unwrap()
{
counter.remove();
}
}

View File

@@ -0,0 +1 @@
pub mod counters_page;

View File

@@ -0,0 +1,18 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::increment_counter(1);
ui::increment_counter(1);
ui::increment_counter(1);
// Then
assert_eq!(ui::total(), 3);
}

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