Compare commits

...

681 Commits

Author SHA1 Message Date
Greg Johnston
2aa14e5e65 differentiate suspense/errorboundary 2023-07-15 16:35:08 -04:00
Greg Johnston
40f6ed9252 make component key paths independent 2023-07-15 12:59:12 -04:00
Greg Johnston
9d9ed45565 make hydration keys !Copy 2023-07-15 10:47:27 -04:00
Greg Johnston
8e68699435 feat: add support for adding CSP nonces (#1348) 2023-07-14 16:37:18 -04:00
Greg Johnston
77580401da fix: hydration-key conflicts between <ErrorBoundary/> children and fallback (#1354) 2023-07-14 14:37:54 -04:00
Joseph Cruz
7902e7edb7 ci: speed up verification (#1347)
* build: introduce ci task
* refactor(ci): rename cargo make task runner
* ci: add ci workflow
* ci: remove redundant workflows
2023-07-14 14:37:17 -04:00
Greg Johnston
4ad223277d fix: duplicated meta content during async rendering (#1352) 2023-07-14 13:14:19 -04:00
Greg Johnston
6f5da11c72 docs/warnings: don't warn when a resource resolves after its scope has been disposed (#1351) 2023-07-14 13:09:57 -04:00
Greg Johnston
3eed86fbf3 docs/warnings: improve ServerFnError when a server function is not found (#1350) 2023-07-14 12:43:08 -04:00
Greg Johnston
10d51a854a v0.4.3 2023-07-14 09:22:19 -04:00
CircuitSacul
6c60bad757 examples: use cfg_attr for conditional derives (#1349)
Revert "use cfg_attr for conditional derives"

This reverts commit b5c85e9ed8a84e5a49f119ae8436d78f2bee1fea.

Revert "Revert "use cfg_attr for conditional derives""

This reverts commit b538256cd69bc2321cdc9066441d71fc94ed80b9.
2023-07-14 09:20:55 -04:00
g-re-g
79f666b5da docs: don't run rust code snippets and update getting started (#1346)
* disable running leptos examples

* typo and small language changes to 02_getting_started
2023-07-14 07:10:59 -04:00
Greg Johnston
3ea3a40395 fix: server_fn rustls feature shouldn't pull in default-tls (#1343) 2023-07-13 19:41:55 -04:00
Greg Johnston
193aa79956 docs: how not to mutate the DOM during rendering (#1344) 2023-07-13 16:57:07 -04:00
Joseph Cruz
3481a6ee53 build: run tasks from workpace or member directory (#1339) 2023-07-13 16:46:51 -04:00
Greg Johnston
d1ef5fce9f fix: un-register <Suspense/> from resource reads when <Suspense/> is unmounted (#1342) 2023-07-13 14:42:05 -04:00
Greg Johnston
5d48911f01 fix: check LEPTOS_OUTPUT_NAME correctly at compile time (#1338) 2023-07-13 10:49:13 -04:00
Greg Johnston
8a90f97959 fix: routing logic to scroll to top was broken (#1335) 2023-07-13 06:43:49 -04:00
Greg Johnston
e9665b34e5 feat: add active_class prop on <A/> (#1323) 2023-07-12 16:21:07 -04:00
Greg Johnston
d4b1ceda90 fix: event delegation issue with <input name="host"> (#1332) 2023-07-12 16:20:11 -04:00
Greg Johnston
a0fae88f7d docs: clarify nightly in "Getting Started" (#1330) 2023-07-12 11:56:45 -04:00
Greg Johnston
03a8609680 fix: warning generated by new #[must_use] on views (#1329) 2023-07-12 10:00:05 -04:00
g-re-g
3e40f9cc66 chore: bump indexmap to 2 (#1325) 2023-07-11 15:24:21 -04:00
Greg Johnston
576bb078f7 fix: Actix server fn redirect() duplicate Location headers (#1326) 2023-07-11 13:57:44 -04:00
Filip Dutescu
3cdcc85c87 feat(config): implement common traits for Env (#1324)
In order to facilitate its usage in other structs, which might derive
various traits, such as `serde::Serialize` or `PartialEq`, implement
them for the `leptos_config::Env` enum.

Signed-off-by: Filip Dutescu <filip@hucksy.dev>
2023-07-11 09:37:42 -04:00
Greg Johnston
ec3a26dfbc fix: <ActionForm/> should set value even if redirected (#1321) 2023-07-11 09:37:13 -04:00
Filip Dutescu
c755dae6ee feat(leptos-config): kebab-case via serde's rename_all (#1308)
Currently, `leptos::LeptosOptions` are deserialized (and serialized)
to `snake_case`, since, by default, `serde` uses the name of the field
as-is. The options given via `.toml` files are given using `kebab-case`.
This behaviour leads to problems if you wish to use
`leptos::LeptosOptions` as the type of a field in your own config,
which uses the `kebab-case` syntax, as it will not provide any values
to that field.

These changes switch out the previous mechanism of manually replacing
the `-` character to the `_` character in order for serialization to
work. In its place, it uses `serde`'s `#[serde(rename_all = ...)]`
macro to handle the naming convention.

In case other naming conventions are used, a workaround would be to
define a user-owned mirror struct of `leptos::LeptosOptions`, which
would contain the required fields, deserialize it as desired and'
convert it to the latter via, for example, the `From<T>` trait.

Closes: #1295

Signed-off-by: Filip Dutescu <filip@hucksy.dev>
2023-07-11 09:36:55 -04:00
Greg Johnston
b67d51e019 docs: remove empty chapter from book 2023-07-10 07:38:12 -04:00
Joseph Cruz
7a34d6026f refactor(ci): improve the organization of cargo make tasks (#1320)
* refactor(cargo-make): extract node

* refactor(cargo-make): extract lint

* refactor(counters_stable): remove redundant tasks

* docs(cargo-make): remove descriptions

* refactor(counters_stable): streamline stages
2023-07-09 20:47:10 -04:00
Mahesh Bansod
548eac8e60 docs: typo & punctuation (#1316) 2023-07-09 20:35:04 -04:00
Mahesh Bansod
05ac8e861f docs: typo (#1315) 2023-07-09 20:34:36 -04:00
Martinez
7a4d475cca docs: update warnings to remove mention of csr as a default feature (#1313) 2023-07-09 20:32:25 -04:00
Greg Johnston
eea8e60518 docs: clarify WASM target (#1318) 2023-07-09 17:06:41 -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
agilarity
fcb98474b8 examples: fix the lint issues in the counter example (#971) 2023-05-01 17:27:29 -04:00
Greg Johnston
54f7e9366a change/fix: require FromStr errors on Params to be Send + Sync so they are ErrorBoundary compatible (#974) 2023-05-01 17:18:46 -04:00
Matt Crane
ddf9df2b5e change: replace serde_urlencoded with serde_html_form to support Vec<_> in server fn args (#973) 2023-05-01 17:17:45 -04:00
Greg Johnston
7fe9f82d89 v0.3.0-alpha (#968) 2023-04-28 19:30:16 -04:00
Roland Fredenhagen
661adc4027 feat: ```view code block in doc comments for properties (#961) 2023-04-28 16:03:04 -04:00
Roland Fredenhagen
1011c464dc feat: add collect_view(cx) (#956) 2023-04-28 16:02:24 -04:00
Frank Panetta
4b498a3b42 chore: fix typos (#964) 2023-04-28 12:10:48 -04:00
yuuma03
3c90b47e77 fix: allow mounting multiple Leptos apps on same server (#966)
Use a HashMap indexed by base URL to cache route branches on the server.
2023-04-28 12:10:02 -04:00
Greg Johnston
671b1e4a8f docs: note need for serde dependency for server functions (closes #947) (#962) 2023-04-27 17:15:29 -04:00
agilarity
52021be806 tests: add wasm web test and common tasks (#954)
* test: rename web test module

* test: extract wasm-web-test task

* test: introduce common tasks

* test: add web-test and common tasks
2023-04-27 17:00:13 -04:00
Roland Fredenhagen
75a7bd610a fix: escapes in doc comments on component properties (#958) 2023-04-27 16:43:38 -04:00
Greg Johnston
de553cf4fe docs: add note on projecting children (#959) 2023-04-26 20:08:12 -04:00
Greg Johnston
0a65f43789 fix: <ErrorBoundary/> toggling between states (closes issue #820) (#957) 2023-04-26 17:30:30 -04:00
Greg Johnston
0f277c55ec fix: use absolute reference to ::leptos::Scope in case not imported 2023-04-25 16:52:14 -04:00
Greg Johnston
04b01a6ced docs: add note about adding CSS classes 2023-04-25 16:26:08 -04:00
Ben Wishovich
6c3381ce52 feat: add From for RequestParts into Parts for Axum and add an option to ge… (#931) 2023-04-24 20:08:28 -04:00
Roland Fredenhagen
fa2e2248d3 feat: impl FromIterator for View (#945) 2023-04-24 20:07:27 -04:00
jquesada2016
362150a715 feat: implemented IntoView for component props (#948) 2023-04-24 20:05:31 -04:00
agilarity
27b5991ee3 examples: test business logic for counter_without_macros (#927) 2023-04-24 20:04:40 -04:00
Greg Johnston
0a7dbb0ca4 feat: add Actix extract helper (#936) 2023-04-24 20:03:24 -04:00
yuuma03
234861a156 fix: generics on impl From slot to Vec<slot> (#946) 2023-04-24 20:03:03 -04:00
Greg Johnston
78d6d312f8 CI: fix unused variables breaking tests (#950) 2023-04-24 17:19:10 -04:00
Greg Johnston
a1144a5b6b examples: add autofocus in todomvc 2023-04-24 08:07:49 -04:00
Greg Johnston
9723cc466e fix: rust-analyzer/cargo fmt issues with LEPTOS_OUTPUT_NAME 2023-04-24 08:00:36 -04:00
Greg Johnston
79c12c0129 examples: better practice for view types in todos (#940) 2023-04-23 17:33:45 -04:00
Jonas Matser
a08d6bae10 fix: ServerFnError::Serialization error string (#939) 2023-04-23 16:17:20 -04:00
Daniel Santana
39261a276c docs: add the GetCbor and GetJson to server macro documentation. (#938) 2023-04-23 15:42:05 -04:00
Roland Fredenhagen
c471986024 feat: add #[allow(missing_docs)] to children prop in components (#934) 2023-04-23 15:34:42 -04:00
Roland Fredenhagen
d2e3a156e8 fix: link to actual type instead of Into trait for component properties (#932) 2023-04-23 15:33:27 -04:00
Fabian Keller
9badfa997b examples: add timer example with reactive use_interval hook (#925) 2023-04-23 15:27:47 -04:00
Ben Wishovich
72f8bf4e20 feat: remove need for LEPTOS_OUTPUT_NAME env var after compilation (#899) 2023-04-23 15:20:47 -04:00
Greg Johnston
c74b15b120 docs: add section on WASM binary size 2023-04-23 15:07:48 -04:00
Craig Rodrigues
9a4f3ab08c chore: specify dependency version for cached (#929) 2023-04-22 17:51:40 -04:00
Greg Johnston
a0935c169e docs: add some content on server-side rendering (#930) 2023-04-22 15:15:48 -04:00
yuuma03
0e2181fb90 fix: allow nested slots (#928) 2023-04-22 14:14:01 -04:00
Greg Johnston
732ec14302 docs: add use of batch to avoid BorrowMut panic 2023-04-22 07:03:10 -04:00
agilarity
ec95060b6e fix: features related compile error (#919)
`cargo make test` sets the --all-features flag by default. This change
clears it.
2023-04-22 06:50:35 -04:00
J
689afec26e docs: fixed typo in interlude_styling.md (#924) 2023-04-22 06:49:15 -04:00
J
bbf23ea40a docs: removed extra unused code blocks in form.md (#923) 2023-04-22 06:48:28 -04:00
J
34e0a8e47d docs: fixed a minor typo in async readme (#921) 2023-04-22 06:47:44 -04:00
Ben Wishovich
81f330e888 feat: add thorough tracing throughout (#908) 2023-04-22 06:47:11 -04:00
Greg Johnston
e5d657dd55 fix: panic when creating nested StoredValue (#920) 2023-04-22 06:44:25 -04:00
Greg Johnston
f919127a7e fix some issues with animated routing (#889) 2023-04-21 15:33:14 -04:00
Greg Johnston
2001bd808f examples: fix broken counters tests (#915) 2023-04-21 15:26:18 -04:00
yuuma03
f51857cedc feat: add slots (closes #769) (#909) 2023-04-21 14:36:38 -04:00
Greg Johnston
f3b8d27c4f change: add window_event_listener_untyped and deprecate window_event_listener pending 0.3.0 (#913) 2023-04-21 14:14:35 -04:00
Greg Johnston
d3a577c365 cargo fmt 2023-04-21 12:45:08 -04:00
Greg Johnston
b80f9e3871 fix: issue with ordering of class attribute and class=("fancy-name-200", true) (closes #907) (#914) 2023-04-21 12:42:35 -04:00
Greg Johnston
328d42656d docs: compile error on mutually-exclusive features (#911) 2023-04-21 12:25:21 -04:00
Logan B. Nielsen
d3d2cbed7e feat: add typed window event listeners (#910) 2023-04-21 11:43:11 -04:00
agilarity
d6f7aedec1 CI: use cargo make to run tests for examples (#904) 2023-04-21 10:33:12 -04:00
Daniel Santana
7a5a776cb9 feat: get_untracked for node_ref. (#902) 2023-04-19 20:09:54 -04:00
Greg Johnston
06f782aa13 perf: improve router performance on server by calculating route branches once (#898) 2023-04-19 20:09:29 -04:00
Greg Johnston
6b825fec37 fix: erroneous non-reactive access warning in undelegated events (#900) 2023-04-19 20:09:05 -04:00
Greg Johnston
b452d8af40 feat: add ability to mutate resources (closes #856) (#886) 2023-04-19 11:40:46 -04:00
Daniel Santana
e96f1d2129 feat: impl Serialize/Deserialize for ParamsMap (closes #892) (#895) 2023-04-19 06:19:53 -04:00
OvermindDL1
72d6af9c84 fix: use once_cell crate until OnceLock stabilized (closes #890)
* Fixes #890 that was using OnceLock, which is nightly only, by adding the once_cell crate as a dependency.

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

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

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

Fixes: #893

Signed-off-by: Filip Dutescu <filip@hucksy.dev>
2023-04-18 15:30:00 -04:00
Greg Johnston
fe68b47ba2 perf: tiny optimization on primitive child values (#887) 2023-04-17 22:09:10 -04:00
Greg Johnston
384d39543c fix: dispose of scope when server fns return error (closes #862) (#888) 2023-04-17 22:08:47 -04:00
agilarity
225e62d12f examples: split counter without macros web test (#884) 2023-04-17 20:26:31 -04:00
Greg Johnston
3905a2aa60 docs: SSR modes 2023-04-17 17:00:52 -04:00
Greg Johnston
ff6ce2dac0 docs: add SSR mode videos 2023-04-17 16:03:36 -04:00
Greg Johnston
16675cbff2 docs: add chapter on styling 2023-04-17 11:50:50 -04:00
HJin.me
9524c6e289 fix: <For/> rendering error in SSR InOrder/Async Mode (#879) 2023-04-17 10:48:07 -04:00
Mark Catley
bc316c648c feat: add expect_context function (#864)
Most of the time when using use_context it would be a bug if the context
wasn't present and appropriate to panic. This is a convenience function
that has that behavior.
2023-04-17 10:47:50 -04:00
Matt Crane
6753ba21c4 fix: allow server functions to work with non-Cargo build systems with SERVER_FN_OVERRIDE_KEY env var (#878) 2023-04-17 08:46:32 -04:00
Greg Johnston
efbe32e081 feat: add non-animation base classes to <AnimatedOutlet/> and <AnimatedRoutes/> (#877) 2023-04-17 08:12:22 -04:00
Kamil Ogórek
55fd6d44f9 docs: Add per-project toolchain override readme (#876) 2023-04-16 16:30:20 -04:00
Mustafa Zaki Assagaf
90972f2d94 fix: updated nix flakes lock files on session auth axum examples to fix once_cell doesn't compile (#872) 2023-04-15 13:32:59 -04:00
Greg Johnston
7382c7e51c feat: add the ability to specify animations on route transitions (#736) 2023-04-14 18:20:42 -04:00
Greg Johnston
8a6d129575 examples: fix error handling in fetch example (#870) 2023-04-14 16:13:14 -04:00
Stackingttv
e20c77710d docs: fixed typo in life cycle docs (#869) 2023-04-14 15:12:18 -04:00
Greg Johnston
93da88eac0 feat: add ability to set node_ref and pass additional attributes to <Form/> and friends (#853) 2023-04-14 14:25:52 -04:00
agilarity
5072539917 examples: fix counter_without_macros test (#863) 2023-04-14 14:06:53 -04:00
Chris Roth
78c59df1d1 docs: fix match statement (#860) 2023-04-14 14:05:21 -04:00
Greg Johnston
75e40eafb2 docs: add "Life Cycle of a Page Load" 2023-04-14 13:30:53 -04:00
Álvaro Mondéjar
274a1ac5f0 Remove & at the end of params queries (#854) 2023-04-12 17:04:22 -04:00
Greg Johnston
17040a4af4 fix: custom events in SSR mode (#852) 2023-04-12 13:21:36 -04:00
Greg Johnston
b09a5f905e docs: emit error when trying to combine global class and dynamic class in a bugged way (#850) 2023-04-11 21:15:07 -04:00
Greg Johnston
683511f311 clippy 2023-04-11 14:37:54 -04:00
Greg Johnston
151c58733b docs: clean up methods documentation 2023-04-11 14:37:12 -04:00
Greg Johnston
012ff56cd6 fix static text nodes with curly braces in SSR (#849) 2023-04-11 12:46:32 -04:00
Nova
493c805993 feat: Trigger primitive and reactive-system cleanups (#838) 2023-04-10 17:47:52 -04:00
Greg Johnston
764192af36 feat: allow multiple HTTP request methods/verbs (#695) 2023-04-10 16:42:15 -04:00
Greg Johnston
f969fd7eff fix: don't entity-encode HTML special characters inside <script> or <style> (closes #837) (#846) 2023-04-10 13:15:15 -04:00
Greg Johnston
2c7ee0d415 feat: rustls feature for reqwest and any other relevant dependencies (#842) 2023-04-10 13:15:00 -04:00
Snêu
5430c78e18 docss: correct broken MaybeSignal link (#840) 2023-04-10 07:37:41 -04:00
Greg Johnston
6b052557d1 fix: correct todo_app_sqlite README (closes #845) 2023-04-10 07:17:46 -04:00
Nova
70f3edb0f5 fix: fix leaks in memos, and in scope parent tracking (#841) 2023-04-09 16:36:53 -04:00
Greg Johnston
4e1f963750 Merge pull request #831 from novacrazy/main
Various optimizations, size reductions and stability improvements
2023-04-08 09:04:13 -04:00
novacrazy
3c3d3b33f1 Remove unnecessary into 2023-04-07 17:41:27 -05:00
novacrazy
be7b9eea25 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-07 14:09:10 -05:00
Greg Johnston
016ad6b7a6 feat: make __Props imports unnecessary (closes #746) (#828) 2023-04-07 15:06:10 -04:00
Greg Johnston
5dab35447a update README.md 2023-04-07 13:19:35 -04:00
Greg Johnston
63be819533 tests: update benchmarks (#827)
* tests: add Criterion benchmarking and move reactive benchmarks into `leptos_reactive`
* tests: updated SSR benchmarks
2023-04-07 13:04:26 -04:00
Aaron Karras
af8afb1204 perf: use local pools for axum handlers (#815) 2023-04-07 11:35:16 -04:00
Mark Catley
2170be8e01 chore: deny warnings on github actions (#814)
Enabling on all except for checking examples to start. I'll fix those
and add it as a follow up.

Closes #795
2023-04-07 09:28:48 -04:00
Greg Johnston
1187a506dd fix: server functions with url as argument name (closes issue #823) (#825) 2023-04-07 09:28:31 -04:00
Greg Johnston
ff5ceddbe2 fix: correctly pass server fn errors to client (#822) 2023-04-07 08:12:10 -04:00
Greg Johnston
41a5e09caa docs: add sandbox links and max height (#824) 2023-04-07 07:38:12 -04:00
novacrazy
60b96c9118 Couple more inline tweaks 2023-04-07 05:28:50 -05:00
novacrazy
7ccb2d9f44 Simplify SsrMode enum 2023-04-07 05:10:55 -05:00
novacrazy
2c2090a194 Return Cow from as_value_string 2023-04-07 05:09:49 -05:00
novacrazy
de9b2998ac More inlining 2023-04-07 05:09:24 -05:00
novacrazy
29b81a3d50 Another round of inlining 2023-04-07 01:44:18 -05:00
novacrazy
5bc0d89ce7 Cleanup Comment::new 2023-04-07 00:52:35 -05:00
novacrazy
342b10c232 Use Cow for ErrorKey 2023-04-07 00:52:23 -05:00
novacrazy
ba9d3c1602 Another round of inlining 2023-04-07 00:52:11 -05:00
novacrazy
d3b3ce6980 Cleanup benchmarks 2023-04-06 21:56:24 -05:00
novacrazy
4b79a91287 Add profile.release to many examples 2023-04-06 21:53:52 -05:00
novacrazy
de06c9b2ca Fix Box<dyn> casts 2023-04-06 21:52:25 -05:00
novacrazy
84c7d00ea9 Use Cow<'static, str> for Attributes 2023-04-06 21:52:11 -05:00
novacrazy
8f5ae0054d Second round of inlining 2023-04-06 21:39:29 -05:00
Nova
374f0c4e27 Merge branch 'leptos-rs:main' into main 2023-04-06 21:31:41 -05:00
novacrazy
a6170f4da9 First round of inlining 2023-04-06 21:02:40 -05:00
novacrazy
578dd5ef35 Convert bubbles to associated const for more reliable const-eval and dead-code elimination 2023-04-06 20:55:18 -05:00
novacrazy
934a131deb Pull out non-generic code from leptos_dom
Avoids duplicate codegen
2023-04-06 20:52:13 -05:00
novacrazy
5bc1c36e67 Pull out non-generic code in reactive core 2023-04-06 20:32:59 -05:00
novacrazy
b1b9853f92 Replace with_scope_property with push_scope_property
Avoids duplicate codegen
2023-04-06 20:26:58 -05:00
novacrazy
5d6a083d1d Fix nested batching and improve batch/untrack behavior 2023-04-06 20:22:58 -05:00
Bram
9478245986 docs: remove Leptos guide link (same as book?) (#818) 2023-04-06 20:44:26 -04:00
Bram
4c1c12734a docs: publish book during CI (#817) 2023-04-06 14:09:54 -04:00
Greg Johnston
5d3a360456 fix: correctly escape HTML special characters in text nodes during SSR (#812) 2023-04-06 06:52:59 -04:00
novacrazy
b51da35a9a Remove allocation in ScopeDisposer 2023-04-05 21:21:22 -05:00
novacrazy
164dcd1b97 Cleanup signal clone impl 2023-04-05 21:19:04 -05:00
novacrazy
c0964c2b01 Fast path for linear deeply nested children 2023-04-05 21:17:37 -05:00
novacrazy
af5b226e53 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-05 19:30:06 -05:00
Nova
4e7a0db950 perf: optimize memory usage of update methods (#809) 2023-04-05 20:16:53 -04:00
Nova
cee6ed9a9f perf: optimize Runtime::mark_dirty (#808) 2023-04-05 20:16:40 -04:00
Greg Johnston
fa1013f7c3 chore: fix unused variable warning in property now that it's not memoized (#810) 2023-04-05 13:20:16 -04:00
novacrazy
3a1db3a191 Merge branch 'main' of https://github.com/leptos-rs/leptos 2023-04-05 08:39:49 -05:00
Ben Wishovich
8b57ba7aa8 feat: add the ability for server fns to be submitted via GET requests (#789) 2023-04-05 06:47:17 -04:00
Mark Catley
ea638e37f6 fix: unused warning in reactive signal diagnostics (#807) 2023-04-05 06:26:36 -04:00
Nova
4342d45a2f perf: optimize size of RuntimeId when slotmap is not used (#805) 2023-04-05 06:26:17 -04:00
Greg Johnston
fe4d2382b8 fix: prevent router panic on root-level <Redirect/> during route list generation (#801) 2023-04-04 21:36:03 -04:00
Greg Johnston
2a13609eff fix: fixes #802 as a temporary measure without resorting to #803 yet (#804) 2023-04-04 20:50:50 -04:00
Marcus Ofenhed
c2ff1cabf1 feat: Add ability to include options to event listeners (#799) 2023-04-04 20:50:35 -04:00
novacrazy
54370e3153 Reduce size of RuntimeId when slotmap is not used 2023-04-04 19:26:57 -05:00
Mark Catley
e72ed26809 fix: warning in Cargo.toml (#800) 2023-04-04 19:53:05 -04:00
Greg Johnston
64e056ffa9 docs: warn if you are using leptos_meta without features (#797) 2023-04-03 21:07:43 -04:00
Mark Catley
db9b7db53d fix: unused warning on cx in server functions (#794)
When running cargo clippy on server functions that use `cx: Scope` it
has an unused variable error.

It appears that the logic for adding an `#[allow(unused)]` notation is
inverted.
2023-04-03 21:07:30 -04:00
ealmloff
a9e6590b5e fix: server functions with non-copy server contexts (#785) 2023-04-03 07:17:22 -04:00
Greg Johnston
b67121b755 docs: <Form/> component (#792) 2023-04-02 16:50:21 -04:00
Greg Johnston
7bce4de682 fix: issues with nested <Suspense/> (closes #764) (#781) 2023-04-02 15:57:43 -04:00
Greg Johnston
8bdb427133 fix: improvements "untracked read" warnings in untrack, SSR cases (#791) 2023-04-02 15:57:06 -04:00
Patrick Auernig
4c23f3c478 chore: remove unused fs dependency from leptos_config (#787) 2023-04-02 12:29:30 -04:00
Greg Johnston
9502de561b fix: warnings about untracked signal access in <Router/> (#790) 2023-04-02 12:28:58 -04:00
Greg Johnston
210c11a733 docs: add runtime "strict mode" checks that warn if you’re non-reactively accessing a value (#786) 2023-04-01 17:41:25 -04:00
ealmloff
6917027204 fix server functions default macro on stable (#784) 2023-04-01 17:31:56 -04:00
Greg Johnston
e78ce7e6b9 feat: create_blocking_resource (#752) 2023-04-01 11:25:00 -04:00
Greg Johnston
a3327f8841 fix: SVG <title> tag (#783) 2023-04-01 11:24:32 -04:00
Greg Johnston
f727dd773b v0.2.5 (#782) 2023-04-01 11:23:42 -04:00
Greg Johnston
952646f066 Merge pull request #780 from leptos-rs/warn-on-routes-issues
docs: warn if you put something invalid inside `<Routes/>`
2023-03-31 17:13:02 -04:00
Greg Johnston
1e037ecb60 chore: clippy and docs warnings (#779) 2023-03-31 17:12:42 -04:00
Greg Johnston
c9f75d82d6 docs: warn if you add something that's not a <Route/> inside <Routes/> 2023-03-31 16:39:06 -04:00
Greg Johnston
de3849c20c example: show how to refactor routes into another component 2023-03-31 16:38:49 -04:00
Christian Rausch
c391c2e938 feat: arbitrary attributes to <Html/> and <Body/> meta tags (#726) 2023-03-31 16:30:10 -04:00
luoxiaozero
1cde4b1f8a docs: fixed parentheses and formatting issues (#775) 2023-03-31 15:48:29 -04:00
Greg Johnston
42360d109b change: insert <head> metadata tags at the beginning of the head, not the end (#731) 2023-03-31 14:51:27 -04:00
Kaszanas
7aa4d9e6db feat: Added `<ProtectedRoute/> component to route file (#741) 2023-03-31 14:50:46 -04:00
Kaszanas
9ed3390b81 examples: updated proxy settings in login_with_token_csr_only (#771)
When testing this example on Windows OS the initial value of `0.0.0.0:3000` for the IP did not work.
2023-03-31 14:44:06 -04:00
Greg Johnston
1ff56f7bfd fix: stop memoizing properties in a way that breaks prop:value (closes #768) (#772) 2023-03-30 19:44:38 -04:00
Greg Johnston
16917997cd fix: prevent forms from entering infinite loops (closes issue #760) (#762) 2023-03-30 16:28:49 -04:00
Greg Johnston
f42568d262 fix: <Redirect/> between nested routes at same level (#767) 2023-03-30 16:28:32 -04:00
Houski
97bbdf561a feat: added the id attribute to the Leptos router <A/> tag (#770) 2023-03-30 16:28:08 -04:00
Greg Johnston
f4043cbd9f fix: escape </script> and other HTML tags in serialized resources (#763) 2023-03-29 13:51:48 -04:00
Lukas Potthast
e9ff26abb4 feat: allow component declaration without use leptos::Scope in scope (#748) 2023-03-29 07:59:08 -04:00
Ben Wishovich
e6b1298915 feat: add property field to Meta component (#759) 2023-03-28 09:10:00 -04:00
Igor Shevchenko
98a9ec8335 chore(docs): fix a few typos (#756) 2023-03-27 20:06:34 -04:00
jquesada2016
5329561687 feat: add is_mounted and dyn_classes (#714) 2023-03-27 19:03:59 -04:00
Greg Johnston
89ca047f2f examples: improve counter_without_macros (#751) 2023-03-27 12:50:01 -04:00
Greg Johnston
a94711fcf0 fix: correct typecast on Memo::get_untracked (closes issue #754) (#755) 2023-03-27 11:28:40 -04:00
Greg Johnston
97d88c65ae docs: warn when reading resource outside <Suspense/> (closes issue #742) (#743) 2023-03-25 14:22:22 -04:00
Jessie Chatham Spencer
e482e3748d docs: document cargo workspace feature resolver footgun (#745)
Due to no rust edition being present in a workspac's Cargo.toml, non
WASM compatible code can end up being built for a WASM target.

This commit documents this error and how to resolve it.
2023-03-25 07:34:28 -04:00
István Donkó
8ab9c08448 docs: fix typo in server_fn docs (#740) 2023-03-24 21:42:27 -04:00
Lachlan Wilger
56de70b714 docs: fix typo (#739)
There was a typo in the section of the docs that pointed towards the hackernews example, so I fixed it by add the word "application."
2023-03-24 21:41:59 -04:00
Greg Johnston
38d97babd8 fix: always run dynamic classes after static classes (closes #735) (#738) 2023-03-24 17:38:34 -04:00
martin frances
4cfecb5d82 chore: bump serde-lite from 0.3 to 0.4. (#737) 2023-03-24 16:54:20 -04:00
Michael Clayton
08b5970b2b check EventSource value for Ok to avoid unwrap panic (#732) 2023-03-23 18:41:18 -04:00
Greg Johnston
af20f80b2b docs: fix typo in router docs (#730) 2023-03-22 20:44:58 -04:00
Andrew Chang-DeWitt
c2fdd2cd70 fix: include missing query params in navigation when <ActionForm/> receives a redirect (#728)
Previous solution in #727 included manually inserted `?` when a leading
`?` is present automatically in `Url.search`.
2023-03-22 20:05:21 -04:00
Greg Johnston
286f3eebe4 fix: relative routing should update when navigating between <Outlet/>s (closes issue #725) (#729)
* clear some cruft out of the navigation code
* fix issue #725 (correctly reactively resolving paths)
2023-03-22 19:59:08 -04:00
Álvaro Mondéjar
509223ab2e chore: Upgrade console_log to stable (#724) 2023-03-22 18:21:53 -04:00
Greg Johnston
665b0b8ed2 chore: make wasm-bindgen dependency optional in leptos_reactive (#723) 2023-03-22 17:56:52 -04:00
Greg Johnston
508ad52582 chore: fix clippy warnings (#721)
* `v0.2.4`

* chore: fix clippy warnings
2023-03-21 18:20:29 -04:00
martin frances
cfd5c98f97 clippy: simplify Box::pin() call. (#718) 2023-03-21 09:06:31 -04:00
Greg Johnston
2e63bb1f50 fix: <Transition/> behavior (#717) 2023-03-21 09:05:41 -04:00
Greg Johnston
982c8f6b5a docs: small fixes (#715) 2023-03-20 20:43:04 -04:00
Greg Johnston
12c4c115f3 docs: make is_odd less pretentious 2023-03-20 20:42:46 -04:00
Carlton Gibson
d4d20ecdb0 Used modulo rather than bitwise & for is_odd check.
The modulo operator is less of a head-scratcher for folks coming through here. The bitwise & is equally correct (clearly) but is likely to cause confusion if folks don't immediately see what's going on.
2023-03-20 20:09:02 +01:00
Greg Johnston
b78919c6ed Merge pull request #712 from leptos-rs/warnings 2023-03-20 10:49:00 -04:00
Greg Johnston
abb9320e31 chore: clear warning and add exports of helpers with handles 2023-03-20 09:36:14 -04:00
Greg Johnston
875d2d5a3a chore: handle unbounded_send warnings 2023-03-20 09:33:58 -04:00
Greg Johnston
42a58855a0 feat: add Scope::batch() (#711) 2023-03-20 08:29:18 -04:00
Greg Johnston
9d142758ec feat: allow manual signal disposal before the scope is disposed (#710) 2023-03-19 21:40:16 -04:00
Greg Johnston
2faddd85cb feat: add set_interval_with_handle and deprecate set_interval (#709) 2023-03-19 16:45:22 -04:00
martin frances
ddd463748d clippy: less .clone() calls, simpler pointer passing. (#707) 2023-03-19 15:30:12 -04:00
Alexis Fontaine
71ee4cd09d fix: view! macro not compiling with a non-default scope name (#704) 2023-03-19 13:14:47 -04:00
Greg Johnston
08c56f7d6c feat: add a debounce helper for event listeners (#691) 2023-03-19 07:10:56 -04:00
Elliot Waite
e1ba26b62c feat: add request_animation_frame_with_handle and request_idle_callback_with_handle (#698) 2023-03-18 19:09:36 -04:00
Greg Johnston
309f0bf826 fix: ignore view markers in DynChild hydration (closes issue #697) (#703) 2023-03-18 19:01:56 -04:00
Greg Johnston
1698ffa7db fix issues in release mode (closes #700) (#701) 2023-03-18 11:04:06 -04:00
Greg Johnston
556955cf1a docs: beginning work on router docs (#682) 2023-03-18 07:34:43 -04:00
Elliot Waite
a9f778459a examples: remove duplicate console_error_panic_hook::set_once() calls (#692) 2023-03-17 16:27:24 -04:00
Greg Johnston
f2ac412253 feat: support diffing inside component children in hot-reload (#690) 2023-03-17 13:53:53 -04:00
Greg Johnston
3bd52fcc9d fix: hydration errors with <Suspense/> inside components in SSR mode (#688) 2023-03-17 12:46:04 -04:00
Vassil "Vasco" Kolarov
b9bd1e103c examples: added example using Tailwind, CSR (only) and Trunk (#666) 2023-03-17 12:45:49 -04:00
Greg Johnston
55f9081465 fix: allow multiple <Suspense/> on same page during in-order or async rendering (#687) 2023-03-17 12:05:36 -04:00
ryndin32
0bac16dba0 docs: typos (#685) 2023-03-15 16:40:57 -04:00
Brett Etter
a8a9c575b5 Added IntoView for ReadSignal and RwSignal in the stable feature. (#677) 2023-03-15 16:40:22 -04:00
Greg Johnston
15ec855db5 Update README.md 2023-03-15 14:34:18 -04:00
Greg Johnston
b8f79a7e56 fix: suppress spurious hydration warnings for tags in leptos_meta (#684) 2023-03-14 14:17:23 -04:00
Greg Johnston
b988ee85f4 fix: leaking stored values (#683) 2023-03-14 11:06:36 -04:00
Greg Johnston
d6e166f105 CI: add --release checks (#681) 2023-03-13 22:19:10 -04:00
Greg Johnston
53ceca8ff8 feat: maintain order of sources and dependencies (#678) 2023-03-13 20:01:03 -04:00
Brett Etter
f2f9759138 fix: release mode (#679) 2023-03-13 20:00:40 -04:00
Greg Johnston
817152ff39 feat: new reactive system implementation (#637) 2023-03-13 17:58:00 -04:00
Greg Johnston
38daaf3b72 chore: apply cargo machete systematically (#671) 2023-03-13 10:16:20 -04:00
Greg Johnston
666d53e2ba feat: <ActionForm/> improvements (#676) 2023-03-13 10:16:02 -04:00
Greg Johnston
b55e9a9e64 v0.2.3: fix broken stable support (#670) 2023-03-13 07:25:08 -04:00
Greg Johnston
8f94c8e6b1 v0.2.2 (#667) 2023-03-12 14:59:04 -04:00
martin frances
604ba3ff90 clippy: signal_wrappers_read, was using .clone() when copy is available. (#665) 2023-03-12 14:52:13 -04:00
Elliot Waite
2e671887d9 docs: typo fixes and other small changes to the docs (#662) 2023-03-12 14:51:47 -04:00
Greg Johnston
e7d56b76b8 fix: apply patches to all instances of a view, not just the first one (#663) 2023-03-11 16:34:13 -05:00
Greg Johnston
87d5bddb21 fix: text node issue in template macro (#661) 2023-03-11 14:25:38 -05:00
Charles Taylor
9d46b7bcb0 feat: impl Copy & Clone for MaybeSignal (#660) 2023-03-11 13:17:16 -05:00
Greg Johnston
591212a56a feat: add fragment support for hot reloading and fix some stuff (#659) 2023-03-11 07:21:37 -05:00
Ben Wishovich
1a3c1e9e52 feat: provide Request<_> in context for Axum, enabling easier extractor use (#632) 2023-03-10 17:28:32 -05:00
martin frances
94998aa95e chore: cargo machete: leptos_macro - Removed unused crates. (#656) 2023-03-10 09:44:23 -05:00
Greg Johnston
c782017578 feat: impl IntoView for &Fragment (#655) 2023-03-10 09:43:03 -05:00
Pikhosh
d291cdb968 fix: show console error instead warning for error! (#654) 2023-03-10 09:42:44 -05:00
ealmloff
29fb1842a5 feat: make server functions work outside of WASM (#643) 2023-03-09 18:03:57 -05:00
Greg Johnston
b085a6c38e docs: add create_effect chapter (#653) 2023-03-09 18:03:38 -05:00
zack.shen
17c12823db docs: spelling error (#651) 2023-03-09 16:45:35 -05:00
martin frances
81401a738c chore: bumped typed-builder up to 0.14. (#648) 2023-03-09 16:44:27 -05:00
martin frances
c9476af063 chore: bump bytecheck to 0.7, remove deprecated simdutf8_std. (#647)
* bump bytecheck to 0.7, remove deprecated simdutf8_std.

* When using rkyv, must use the appropiate CheckBytes.
2023-03-09 16:44:06 -05:00
Greg Johnston
1f95eb1e1d chore: typo (closes issue #645) (#646) 2023-03-08 19:52:51 -05:00
Vanius Bittencourt
1584ab6b72 feat: refactor leptos_config to allow loading from string (#628) 2023-03-08 19:49:19 -05:00
martin frances
c63c8ac447 chore: cargo machete: Strip down leptos_server. (#644) 2023-03-08 19:37:22 -05:00
martin frances
1a0e2b509e chore: bump serde-wasm-bindgen to 0.5. (#639) 2023-03-08 19:12:30 -05:00
martin frances
c66b673067 chore: <Form/> component Removed unused variables. (#640) 2023-03-07 14:04:55 -05:00
martin frances
a13468228a Bumped tower-http upto 0.4. (#638) 2023-03-07 14:03:54 -05:00
Greg Johnston
bb0324fd48 fix: custom events (closes issue #641) (#642) 2023-03-07 14:00:48 -05:00
jo!
369ea8531f examples: add session_auth_axum (#589) 2023-03-06 21:16:56 -05:00
Greg Johnston
9cc28943aa CI: split into three actions (#636) 2023-03-06 21:00:56 -05:00
erwanvivien
10bdff7d4b de-duplicate todomvc example (#634) 2023-03-06 16:52:35 -05:00
martin frances
27fb430900 bump typed-builder to version 0.13. (#633) 2023-03-06 09:07:21 -05:00
jfloresremar
207dedab6e Update 04_iteration.md (#630) 2023-03-06 09:06:58 -05:00
IchHabeKeineNamen
0052b10df3 docs: fix instruction typos (#631) 2023-03-06 09:05:21 -05:00
Greg Johnston
08d98691a3 fix: boolean attributes in SSR (#629) 2023-03-04 14:24:08 -05:00
WafflePersonThing
34aa0e014b fix: added missing attributes of events that don't bubble (#625)
references used:
- https://developer.mozilla.org/en-US/docs/Web/API/
- web archives of the above before jun 11th 2022, relevant: https://github.com/mdn/content/issues/19590
2023-03-04 11:30:12 -05:00
Greg Johnston
55ce805b60 feat: hot reloading support for cargo-leptos (#592) 2023-03-04 09:04:22 -05:00
Greg Johnston
1e0adcd89a docs: add a chapter on async actions and create_action (#623) 2023-03-03 17:25:19 -05:00
Greg Johnston
4e67b3aef8 CI: exclude rkyv combos with other serialization traits (#622) 2023-03-03 15:48:06 -05:00
Greg Johnston
02e2948e00 fix: suppress warnings caused by resource loading in generate_route_list (closes #582) (#621) 2023-03-03 13:20:38 -05:00
Greg Johnston
bd86125629 feat: allow easier client-side form validation (closes #413) (#620) 2023-03-03 13:19:54 -05:00
Greg Johnston
11d9018e4f docs: add patterns for global state (closes #245) (#619) 2023-03-03 11:51:21 -05:00
Greg Johnston
553e38ba15 tests: use check instead of build in CI for disk space (#616) 2023-03-03 11:50:50 -05:00
Greg Johnston
c8e6d18139 feat: allow multiple class names in view! macro class = (closes #612) (#614) 2023-03-03 10:44:15 -05:00
Greg Johnston
e29f6a884f docs: improve "Getting Started" page (#618) 2023-03-03 10:43:49 -05:00
Greg Johnston
10a2ada42a add note about running Trunk from root 2023-03-03 10:42:18 -05:00
martin frances
2dd9116a20 chore: clippy - simplified conditional logic in transition.rs. (#615) 2023-03-03 09:06:56 -05:00
Roland Fredenhagen
2ee323135f feat: support expressions in #[prop(default=...)] (#611) 2023-03-02 19:15:45 -05:00
Ivan Agafonov
cebc824fbe docs: updated error handling code (#610)
code is from already updated example
2023-03-02 07:24:04 -05:00
Sergei Gnezdov
dd0bcb950a docs: fix compilation error, Issue #608 (#609)
Compiler reports error
F may not live long enough
2023-03-02 07:23:35 -05:00
Greg Johnston
bb5ad101a2 publish framework-independent server_fn crate (#605) 2023-03-02 07:22:36 -05:00
Ivan Agafonov
df90f183fd docs: use create_node_ref instead of NodeRef::new (#607)
Code in the example already updated by someone
2023-03-02 07:22:18 -05:00
ealmloff
0c261c0fb0 feat: make server functions framework agnostic (#596) 2023-03-01 20:56:30 -05:00
Greg Johnston
f46084723a fix: memory leak in streaming SSR (closes issue #590) (#601) 2023-03-01 20:54:28 -05:00
Qwox
08ad6832af fix: set new value before resetting input (#604)
Co-authored-by: Qwox <qwox@qwox.com>
2023-03-01 20:04:37 -05:00
Greg Johnston
700a110350 Merge pull request #603 from makoven/patch-1
Fix typo in 03_components.md
2023-03-01 16:29:46 -05:00
Artem Makoven
33b5d8c4fb Fix typo in 03_components.md 2023-03-02 05:54:59 +09:00
Greg Johnston
ceb7bd398d Merge pull request #602 from iagafonov/patch-1 2023-03-01 14:26:49 -05:00
Ivan Agafonov
7f72c804f4 typo
_cx replaced with cx
2023-03-01 20:08:03 +03:00
Greg Johnston
be0a793179 Merge pull request #599 from leptos-rs/hydration-do-not-reset
fix: SSR + hydration improvements
2023-03-01 06:00:50 -05:00
Greg Johnston
e1625106b8 fix SSR tests 2023-02-28 21:35:59 -05:00
Greg Johnston
abef12279b fix: don't re-set attributes found in HTML during hydration (closes #597) 2023-02-28 19:56:13 -05:00
Greg Johnston
578853877a fix: restore SSR fast-path support 2023-02-28 15:36:52 -05:00
Greg Johnston
04eae63e39 examples: include missing examples in CI (#598) 2023-02-28 15:33:02 -05:00
Brendon Otto
4b98ece2b4 example: update README.md (#595)
Incorrect framework referenced
2023-02-28 09:45:10 -05:00
Greg Johnston
1b2a0fe2ad fix: mouseenter and mouseleave do not bubble (#593) 2023-02-28 09:39:52 -05:00
Thomas Kratz
ab6ddc1194 fix: make counter test compile (#588) 2023-02-27 21:50:12 -05:00
Azz
b153ab51ee feat: support rkyv encoding (#577) 2023-02-26 16:12:53 -05:00
Greg Johnston
34f7b90177 perf: improvements to event delegation and element creation in <For/> (#579) 2023-02-26 08:31:03 -05:00
g-re-g
3648af0d9d fix: correct scheme handling in router, and improve matching code by removing regexes (#569) 2023-02-26 07:15:14 -05:00
Greg Johnston
3d50ca32cd v0.2.0 2023-02-25 15:45:35 -05:00
tanguy-lf
e576d93f83 examples: add ssr_mode_axum (#575) 2023-02-25 11:24:24 -05:00
Greg Johnston
e71779b8a6 fix: <Transition/> with local_resource (closes #562) (#574) 2023-02-24 19:51:03 -05:00
Markus Kohlhase
0301c7f1cf example: Login with API token (CSR only) (#523) 2023-02-24 17:11:58 -05:00
Remo
46e6e7629c chore: macro panic hygiene (#568) 2023-02-24 16:36:05 -05:00
SleeplessOne1917
a985ae5660 fix: <Meta/> component as_ property outputs correct attribute html (#573) 2023-02-24 08:58:15 -05:00
Denis Nazarov
efe29fd057 Relax Eq to PartialEq for create_slice() (#570)
Co-authored-by: Denis Nazarov <denis.nazarov@gmail.com>
2023-02-23 08:00:01 -05:00
Greg Johnston
05b5b25c0e fixes issue #565 (#566) 2023-02-22 09:20:33 -05:00
Greg Johnston
46f6deddbf fix: transition fallback (closes #562) (#563) 2023-02-21 15:11:08 -05:00
Fangdun Tsai
e9c4b490e5 feat: viz integration (#506) 2023-02-21 12:29:15 -05:00
PolarMutex
f7d0eea04d feature: add class prop to <Html/> component (#554) 2023-02-21 12:28:11 -05:00
Greg Johnston
bf246a62e7 fix: issue with local resources blocking <Suspense/> fragments from resolving (#561) 2023-02-21 06:29:29 -05:00
Greg Johnston
b8acfd758d fix: remove unnecessary log (#560) 2023-02-20 21:34:09 -05:00
Greg Johnston
dc8fd37461 docs: add create_resource, <Suspense/>, and <Transition/> (#559) 2023-02-20 17:39:17 -05:00
Greg Johnston
cc2b310e4c docs: add example of <ButtonC on:click/> syntax (#558) 2023-02-20 15:41:56 -05:00
Thomas Versteeg
884348d7f8 doc: fix button name in parent_child example (#555) 2023-02-20 14:53:04 -05:00
Greg Johnston
dafd6e51c5 v0.2.0-beta (#557) 2023-02-20 14:52:31 -05:00
Ben Wishovich
322041917d fix issue with redirects in server fns creating multiple Location headers (#550) 2023-02-20 08:55:47 -05:00
Ikko Eltociear Ashimine
a2eaf9b3ee fix: typo in hydration docs(#552)
identifer -> identifier
2023-02-20 07:09:33 -05:00
Chrislearn Young
4032bfc210 fix: document docs typo (#553) 2023-02-20 07:08:51 -05:00
Greg Johnston
4ff08f042b change: pass Scope as argument into Resource::read() and Resource::with() (#542) 2023-02-19 19:52:31 -05:00
Greg Johnston
ce4b0ecbe1 fix: more work on hydration IDs with <Suspense/> (#545) 2023-02-18 21:20:40 -05:00
Greg Johnston
6c31d09eb2 revert PR #538 (#544) 2023-02-18 18:39:08 -05:00
Greg Johnston
59ad6a4725 revert accident 2023-02-18 16:03:20 -05:00
Greg Johnston
884dacbc6c fix example 2023-02-18 16:02:19 -05:00
Dmitrii Kuzmin
9c572f7617 fix(examples): hackernews_axum styles href (#536) 2023-02-18 15:17:54 -05:00
jquesada2016
487dba90d8 fix: off-by-one error in <For/> (closes #533) (#538) 2023-02-18 14:57:14 -05:00
Greg Johnston
20f24d2f3a fix: building leptos_reactive in release mode (#540) 2023-02-18 12:45:58 -05:00
Greg Johnston
20cbc240ee v0.2.0-alpha2 (#539) 2023-02-18 12:45:46 -05:00
jquesada2016
f2f52b2533 change: move signal method implementations into traits in signal prelude (#490) 2023-02-18 07:30:03 -05:00
Sean Aye
46d6e3f78c fix compile of leptos dom (#535) 2023-02-17 18:25:58 -05:00
Greg Johnston
586f524015 feature: in-order streaming and async rendering (#496) 2023-02-17 17:31:32 -05:00
Greg Johnston
79781ec20c Fix test import location 2023-02-17 16:18:22 -05:00
Greg Johnston
91f6d9a404 What's in a name? 2023-02-17 07:25:42 -05:00
Greg Johnston
76a74ecde2 fix: hydration IDs for elements following <Suspense/> (closes #527) (#531) 2023-02-16 21:12:52 -05:00
Greg Johnston
0071a48b8a feature: reintroduce limited template-node cloning w/ template macro (#526) 2023-02-16 07:02:01 -05:00
Greg Johnston
8d42e91eb8 fix: top-level SVG in view macro with new exports (#525) 2023-02-15 15:38:06 -05:00
Greg Johnston
00a796d204 change: tweak API of Errors and implement IntoIter (#522) 2023-02-15 14:03:16 -05:00
henrik
bde585dc3e feature: enable cargo-leptos to reload multiple CSS files (#524) 2023-02-14 18:51:47 -05:00
Greg Johnston
0a534bd7fd Reexport web-sys event types in leptos::ev to make it easier to type handlers (#521) 2023-02-13 20:45:46 -05:00
Greg Johnston
50d8eae694 fix: correct namespace for Unit in empty views (closes #518) (#520) 2023-02-13 20:25:26 -05:00
martin frances
e732a4952b leptos_dom erros.rs remove<E>() does not need to be generic. (#516)
* leptos_dom erros.rs remove<E>() does not need to be generic.

* fixed up errors.remove().
2023-02-13 20:25:11 -05:00
Greg Johnston
8a99623fd6 0.2.0-alpha (#515) 2023-02-13 07:49:29 -05:00
Greg Johnston
7d6c4930e4 remove .unwrap() from redirect in Actix integration (#514) 2023-02-13 06:02:43 -05:00
IcosaHedron
81d6689cc0 do not unwrap use_context in integrations axum redirect (#513) 2023-02-12 21:59:12 -05:00
Greg Johnston
989b5b93c3 CI: fix Wasm testing (#511) 2023-02-12 19:39:32 -05:00
Greg Johnston
ca510f72c1 fix: SSR export in Wasm mode (#512) 2023-02-12 19:12:15 -05:00
Greg Johnston
6dd3be75d1 fix: import in leptos_dom and add Wasm build to CI for regressions (#510) 2023-02-12 18:58:57 -05:00
g-re-g
51e11e756a Typos and a small cleanup (#509) 2023-02-12 18:11:31 -05:00
Greg Johnston
1dbcfe2861 change: reorganize module exports and reexports (#503) 2023-02-12 17:04:36 -05:00
Greg Johnston
db3f46c501 Add docs on testing (closes #489) (#508) 2023-02-12 17:03:12 -05:00
Greg Johnston
1cba54d47e fix: <For/> in todomvc example (#504) 2023-02-11 16:30:09 -05:00
Greg Johnston
d1ae3b49cc docs: further additions (#505) 2023-02-11 15:55:43 -05:00
Greg Johnston
6bab4ad966 apply new formatting everywhere (#502) 2023-02-11 14:30:06 -05:00
jquesada2016
d4648da5c6 chore: add workspace rustfmt.tml (#483) 2023-02-11 14:25:55 -05:00
Greg Johnston
cf7deaaea3 fix: proper disposal of nested route scopes (#499) 2023-02-11 14:12:59 -05:00
g-re-g
d0cacecfc6 Allow literal string as class in view macro (#500) 2023-02-10 22:43:40 -05:00
Greg Johnston
ce2c3ec97c examples: remove unused index.html (#497) 2023-02-10 08:02:26 -05:00
martin frances
b9f05f94ce chore: remove unused .clone() call in <Suspense/>. (#486) 2023-02-08 20:44:10 -05:00
Greg Johnston
fe7aacb0c8 Handle <ErrorBoundary/> hydration correctly (closes #456) 2023-02-08 20:32:59 -05:00
Greg Johnston
3fd3e73a10 Correctly handle custom elements in SSR 2023-02-08 20:32:59 -05:00
Greg Johnston
7dca740e47 Add error boundary example to list 2023-02-08 20:32:59 -05:00
Greg Johnston
73420affed Basic error boundary example 2023-02-08 20:32:59 -05:00
Greg Johnston
7c25f59a68 Update README.md 2023-02-08 20:32:32 -05:00
Greg Johnston
c24874d9c8 change: add Scope to view function in <For/> to avoid memory "leak" (#492) 2023-02-08 20:28:04 -05:00
Greg Johnston
4759dfcb60 missing ; 2023-02-08 14:34:57 -05:00
Greg Johnston
ca9419b53f fix: fix debug_warn behavior in reactive crate and remove log dependency (#491) 2023-02-08 07:04:01 -05:00
jquesada2016
765006158a change: NodeRef<HtmlElement<Div>> generics to NodeRef<Div> (#481) 2023-02-07 20:13:25 -05:00
Greg Johnston
8a1adaefaf fix: typed route params with #[derive(Params)] (#488) 2023-02-07 17:28:46 -05:00
Greg Johnston
086326324e Fix inner_html in SSR (#487) 2023-02-07 13:14:14 -05:00
martin frances
e59ee6329e Minor: Clippy router now uses types OnFormData and OnResponse. (#484) 2023-02-07 09:52:29 -05:00
Greg Johnston
a2b31a51d9 fix: errors on 404 page in axum_errors example (#485) 2023-02-07 09:51:52 -05:00
Jan
b0a98d8b4f Better styling for router related components (#477) 2023-02-06 18:34:39 -05:00
Greg Johnston
6931d3904b remove unnecessary "openssl" feature from Actix examples (#480) 2023-02-06 09:10:09 -05:00
Greg Johnston
e380097a9e Create README.md 2023-02-05 21:54:16 -05:00
Greg Johnston
44c18da324 docs: (in-progress) new tutorial/guide format with integrated CodeSandboxes (#375) 2023-02-05 21:33:42 -05:00
Greg Johnston
256cf0c59b Remove old book 2023-02-05 21:28:52 -05:00
Greg Johnston
0765e51db8 fix: adding/removing errors from <ErrorBoundary/> (#478) 2023-02-05 21:23:02 -05:00
Greg Johnston
45d4ebccd8 fix: cargo doc in projects using #[server] (#476) 2023-02-05 19:12:32 -05:00
Greg Johnston
352601aa42 fix: correct out-of-order streaming behavior (#475) 2023-02-05 17:29:35 -05:00
g-re-g
7f77910e91 impl From<&str> for MaybeSignal<String> (#472) 2023-02-04 16:47:40 -05:00
Ben Wishovich
76aeb573bf fix: convert site_address to site_addr to match cargo-leptos (#462) 2023-02-04 16:37:41 -05:00
Greg Johnston
e0bf8f5b6d fix: fix node_ref in SSR (#471) 2023-02-04 15:37:59 -05:00
Greg Johnston
5ace580edb fix: don't override element event listeners with component event listeners (closes #461) (#470) 2023-02-04 15:37:48 -05:00
Roland Fredenhagen
5d612d9740 error on non meta input for prop attribute (#469) 2023-02-04 13:17:04 -05:00
John Funk
eacff684ef Add simple icon logo (#468) 2023-02-04 10:19:33 -05:00
Greg Johnston
4034aa9c11 feature: add isomorphic <Redirect/> component (closes #412) (#466) 2023-02-04 10:02:17 -05:00
Roland Fredenhagen
45275ff8d4 impl Default for MaybeSignal (#464) 2023-02-04 10:01:55 -05:00
Greg Johnston
3ff5089bf4 docs: note about optional fallback (closes #406) (#463) 2023-02-04 08:34:38 -05:00
Jan
c28297fe93 Do it on an other branch (#460) 2023-02-04 07:12:53 -05:00
Greg Johnston
6d0d70cd17 perf: further reduce WASM binary size by ~5-7% (#459)
* Update `leptos_router` docs
* Further reducing WASM bundle sizes
2023-02-03 17:38:44 -05:00
g-re-g
c4e693e01e Derive debug in server macro (#458) 2023-02-03 17:38:29 -05:00
Greg Johnston
2be4e8d959 docs: add new Children types to macro docs (#454) 2023-02-03 12:51:37 -05:00
Odiseo
fec4ff4381 fix: typo in leptos_config description (#455) 2023-02-03 12:51:26 -05:00
Greg Johnston
25c313aeb5 fix: stack overflow in with nested outlet (closes #452) (#453) 2023-02-03 11:03:02 -05:00
martin frances
0dbcc323ba Clippy: "{input} is not a supported environment. (#451) 2023-02-03 10:08:23 -05:00
Greg Johnston
6b683f9ab6 fix: leptos_router hydration issues (#450) 2023-02-03 06:50:36 -05:00
Tobias Goulden Schultz
aae4d4445e fix: update leptos dependencies to point to the same workspace as other examples (#449) 2023-02-02 23:24:22 -05:00
Greg Johnston
bb9df8937d feature: allow on: event listeners on <Component/> nodes (#448) 2023-02-02 23:24:03 -05:00
Greg Johnston
05277f03b6 fix: successfully pass context to nested routes via <Outlet/> (#447) 2023-02-02 21:00:32 -05:00
Gentle
f698f8badd use latest tokio in leptos_axum (#443) 2023-02-02 17:00:49 -05:00
martin frances
98f51fec8a router: Machete - Removed unused deps. (#442) 2023-02-02 17:00:12 -05:00
martin frances
65465cad78 leptos_macro: Machete - Removed unused deps. (#441) 2023-02-02 16:59:49 -05:00
martin frances
ddee545e7e leptos-server: Removed dependecy on log, linear-map, rmp-serde. (#439) 2023-02-02 16:59:07 -05:00
g-re-g
cbfb724af2 Dedup from_str implementations for Env (#426) 2023-02-02 07:18:20 -05:00
Greg Johnston
0953007f47 fix: correct behavior of <Show/> so it renders correctly when toggling between conditions multiple times, without rerendering on every change (#436) 2023-02-01 20:37:00 -05:00
Greg Johnston
53f7677258 Fix top-level SVG elements in SSR (#435) 2023-02-01 20:36:50 -05:00
Greg Johnston
6373fd42fb Switch examples to check instead of build (for CI resources) and add missing examples (#437) 2023-02-01 20:36:37 -05:00
Greg Johnston
e1bcf77b03 docs: Document inner_html attribute (#429) 2023-02-01 19:21:08 -05:00
Greg Johnston
b0762bbfb5 Make RouteDefinition public (#430) 2023-02-01 19:20:50 -05:00
IcosaHedron
63a7a4dec1 Several Minor Updates on Examples (#427) 2023-02-01 19:20:34 -05:00
jquesada2016
1f6a326268 fixes cx not found on components marked with #[component(transparent)] (#423) 2023-02-01 11:17:20 -05:00
Greg Johnston
0efc39db8b fix: Make all fragment rendering lazy (closes #299 and #421) (#425)
Make all fragment rendering lazy (closes #299 and #421)
2023-02-01 06:47:12 -05:00
Greg Johnston
cbf2f73e95 fix: HTML entity issues in axum_errors example (#424) 2023-01-31 23:39:31 -05:00
Ben Wishovich
160f336303 Update ErrorBoundary to use miette::Diagnostic instead of Error, and various other tweaks (#401)
* Switch RwLock to parking_lot so they are no longer async
* cleanup todo_app_sqlite_axum
* add errors_axum example

---------

Co-authored-by: Indrazar <110272232+Indrazar@users.noreply.github.com>
2023-01-31 21:56:42 -05:00
starmaker
e2b1365e46 Implemented update_returning for StoredValue (#419) 2023-01-31 17:40:39 -05:00
Greg Johnston
45eee12b18 Fix issues with attribute names in SSR (#418) 2023-01-31 11:57:05 -05:00
Bruno De Simone
e2cdbc746f Add leptos_routes functions for integrations (#415)
* added leptos_routes_with_context

* added leptos_routes_with_handler for axum integration
2023-01-31 09:09:58 -05:00
Ben Wishovich
48cf8d9382 Switch RwLock to parking_lot so they are no longer async (#414) 2023-01-30 20:11:56 -05:00
Greg Johnston
42e50327a6 Fix <option> and <use> top-level types in SSR (#416) 2023-01-30 20:10:07 -05:00
martin frances
ea0e2ce363 Escape <HTML> and <BODY> tokens in documentation markup. (#410) 2023-01-30 19:17:41 -05:00
martin frances
465cbc36be Minor: Bump typed-builder from 0.11 to 0.12. (#409) 2023-01-30 19:17:09 -05:00
Greg Johnston
62061f90ea Add <Html/> and <Body/> components in leptos_meta (#407)
Closes #376.
2023-01-29 19:07:48 -05:00
Greg Johnston
9a231ddef0 Merge pull request #408 from leptos-rs/fix-boolean-attributes-ssr
Fix boolean attributes in `view` macro fast-path SSR
2023-01-29 18:43:21 -05:00
Greg Johnston
ce6a093f9f oops 2023-01-29 17:11:02 -05:00
Greg Johnston
f07fa0e0be escape attributes 2023-01-29 16:55:28 -05:00
Greg Johnston
43ad91512a Fixes boolean attributes in SSR (closes #405) 2023-01-29 16:29:06 -05:00
Greg Johnston
116d23f2c3 Revert "fix: Fixes boolean attributes in HTML fast-path (closes issue #405)"
This reverts commit 2ecb345a79.
2023-01-29 16:27:28 -05:00
Greg Johnston
2ecb345a79 fix: Fixes boolean attributes in HTML fast-path (closes issue #405) 2023-01-29 16:02:47 -05:00
Greg Johnston
f55f833426 Merge pull request #403 from leptos-rs/children-type-alias
Add `Children` type alias
2023-01-29 07:07:09 -05:00
Greg Johnston
7101a2f55e Add Children type alias 2023-01-28 22:32:00 -05:00
Greg Johnston
f8b76387ec Fix labels in parent_child README 2023-01-28 21:52:16 -05:00
Greg Johnston
11fc51577b 0.1.3 2023-01-28 12:12:09 -05:00
Greg Johnston
ae1ca969ef Merge pull request #397 from leptos-rs/v0.1.2
`v0.1.2`
2023-01-28 11:17:30 -05:00
Greg Johnston
895f9d8487 Missing web-sys types 2023-01-28 08:19:13 -05:00
Greg Johnston
1e45b182a0 Fix <ErrorBoundary/> removal behavior 2023-01-28 08:14:32 -05:00
Greg Johnston
4c26dc597d Docs for <Show/> component 2023-01-28 08:07:23 -05:00
Greg Johnston
2863d49a1c Docs for <ErrorBoundary/> 2023-01-28 07:54:13 -05:00
Greg Johnston
087eb18c8b Merge pull request #396 from leptos-rs/hydration-fix-small-wasm
Fix hydration issue related to WASM size reduction
2023-01-28 07:16:23 -05:00
Greg Johnston
c7c672717c Fix hydration issue related to WASM size reduction 2023-01-28 07:16:08 -05:00
Greg Johnston
c69cc02f30 Merge pull request #393 from leptos-rs/small-wasm
Reduce WASM binary sizes by 3-5%
2023-01-28 07:08:52 -05:00
Greg Johnston
9eb81f00f9 Merge pull request #395 from thomasqueirozb/main
Fix gtk example
2023-01-28 07:08:43 -05:00
Thomas Queiroz
72fe3d45f0 Fix gtk example 2023-01-28 01:14:02 -03:00
Greg Johnston
7802d941bd cargo fmt 2023-01-27 17:04:34 -05:00
Greg Johnston
f10784f686 Merge pull request #391 from leptos-rs/remove-gloo-dependency
Remove `gloo` dependency in `leptos_dom`
2023-01-27 16:58:54 -05:00
Greg Johnston
35197691c0 Merge pull request #392 from martinfrances107/cargo_outdated
doc/book updated leptos version.
2023-01-27 16:58:41 -05:00
Greg Johnston
4afbef87f6 clippy stuff 2023-01-27 16:56:22 -05:00
Greg Johnston
218485e3be Make helpers into concrete functions for WASM binary size purposes 2023-01-27 16:24:05 -05:00
Greg Johnston
8d60a191eb Missing Storage dependency (now that gloo is gone) 2023-01-27 15:36:20 -05:00
Greg Johnston
1ba01a46af Use a concrete helper function to generate elements 2023-01-27 15:33:28 -05:00
Martin
fdece25051 BugFix, ch03 properly construct the "input_element". 2023-01-27 20:04:29 +00:00
Greg Johnston
590056e047 Remove gloo dependency in leptos_dom 2023-01-27 14:01:07 -05:00
Martin
817bb1628e doc/book updated leptos version. 2023-01-27 19:00:31 +00:00
Greg Johnston
f911cdd56f Merge pull request #390 from leptos-rs/action-form-clear-input
fix: Align `<ActionForm/>` behavior with `Action`
2023-01-27 13:53:31 -05:00
Greg Johnston
76a9c719a3 Fix missing docs error (#389) 2023-01-27 12:29:22 -05:00
Greg Johnston
395336a8c0 Correctly set pending state with ActionForm 2023-01-27 12:19:20 -05:00
Greg Johnston
b84906e6dc ActionForm should clear input as Action::dispatch() does 2023-01-27 12:15:50 -05:00
586 changed files with 45279 additions and 12134 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.

46
.github/workflows/check-examples.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Check Examples
on:
push:
branches:
- main
pull_request:
branches:
- main
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: Check
needs: [setup]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"

View File

@@ -1,4 +1,4 @@
name: Test
name: Check stable
on:
push:
@@ -8,15 +8,16 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
- stable
os:
- ubuntu-latest
@@ -30,17 +31,16 @@ jobs:
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
- name: Run Rustfmt
run: cargo fmt -- --check
- uses: Swatinem/rust-cache@v2
- name: Run tests with all features
run: cargo make ci
- name: Run cargo check on all examples
run: cargo make --profile=github-actions check-stable

38
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
matrix-job:
name: CI
strategy:
matrix:
directory:
[
integrations/actix,
integrations/axum,
integrations/viz,
integrations/utils,
leptos,
leptos_config,
leptos_dom,
leptos_hot_reload,
leptos_macro,
leptos_reactive,
leptos_server,
meta,
router,
server_fn,
server_fn/server_fn_macro_default,
server_fn_macro,
]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"

37
.github/workflows/publish-book.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Deploy book
on:
push:
paths: ['docs/book/**']
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write # To push a branch
pull-requests: write # To create a PR from that branch
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install mdbook
run: |
mkdir mdbook
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.27/mdbook-v0.4.27-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
- name: Deploy GitHub Pages
run: |
cd docs/book
mdbook build
git worktree add gh-pages
git config user.name "Deploy book from CI"
git config user.email ""
cd gh-pages
# Delete the ref to avoid keeping history.
git update-ref -d refs/heads/gh-pages
rm -rf *
mv ../book/* .
git add .
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
git push --force --set-upstream origin gh-pages

View File

@@ -0,0 +1,95 @@
name: Run 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-
# Run Cargo Make Task
- 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

@@ -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-cargo-make-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-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

2
.gitignore vendored
View File

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

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,40 +1,47 @@
[workspace]
resolver = "2"
members = [
# core
"leptos",
"leptos_dom",
"leptos_config",
"leptos_hot_reload",
"leptos_macro",
"leptos_reactive",
"leptos_server",
"server_fn",
"server_fn_macro",
"server_fn/server_fn_macro_default",
# integrations
"integrations/actix",
"integrations/axum",
"integrations/viz",
"integrations/utils",
# libraries
"meta",
"router",
# book
"docs/book/project/ch02_getting_started",
"docs/book/project/ch03_building_ui",
"docs/book/project/ch04_reactivity",
]
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.1.1"
version = "0.4.3"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.1.1" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.1" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.1" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.1" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.1" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.1" }
leptos_router = { path = "./router", version = "0.1.1" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.1" }
leptos = { path = "./leptos", version = "0.4.3" }
leptos_dom = { path = "./leptos_dom", version = "0.4.3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.3" }
leptos_macro = { path = "./leptos_macro", version = "0.4.3" }
leptos_reactive = { path = "./leptos_reactive", version = "0.4.3" }
leptos_server = { path = "./leptos_server", version = "0.4.3" }
server_fn = { path = "./server_fn", version = "0.4.3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.4.3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.3" }
leptos_config = { path = "./leptos_config", version = "0.4.3" }
leptos_router = { path = "./router", version = "0.4.3" }
leptos_meta = { path = "./meta", version = "0.4.3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.3" }
[profile.release]
codegen-units = 1

View File

@@ -3,45 +3,25 @@
# cargo install --force cargo-make
############
[config]
# make tasks run at the workspace root
default_to_workspace = false
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
[tasks.ci]
dependencies = ["build", "build-examples", "test"]
[tasks.build]
clear = true
dependencies = ["build-all"]
[tasks.build-all]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.build-examples]
[tasks.check-stable]
workspace = false
clear = true
dependencies = [
{ name = "build", path = "examples/counter" },
{ name = "build", path = "examples/counter_isomorphic" },
{ name = "build", path = "examples/counters" },
{ name = "build", path = "examples/counters_stable" },
{ name = "build", path = "examples/fetch" },
{ name = "build", path = "examples/hackernews" },
{ name = "build", path = "examples/hackernews_axum" },
{ name = "build", path = "examples/parent_child" },
{ name = "build", path = "examples/router" },
{ name = "build", path = "examples/tailwind" },
{ name = "build", path = "examples/todo_app_sqlite" },
{ name = "build", path = "examples/todo_app_sqlite_axum" },
{ name = "build", path = "examples/todomvc" },
{ name = "check", path = "examples/counter_without_macros" },
{ name = "check", path = "examples/counters_stable" },
]
[tasks.test]
clear = true
dependencies = ["test-all"]
[tasks.test-all]
[tasks.ci-examples]
workspace = false
cwd = "examples"
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
args = ["make", "ci-clean"]
[tasks.clean-examples]
workspace = false
cwd = "examples"
command = "cargo"
args = ["make", "clean"]

View File

@@ -6,6 +6,9 @@
[![crates.io](https://img.shields.io/crates/v/leptos.svg)](https://crates.io/crates/leptos)
[![docs.rs](https://docs.rs/leptos/badge.svg)](https://docs.rs/leptos)
[![Discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord)](https://discord.gg/YdRAhS7eQB)
[![Matrix](https://img.shields.io/badge/Matrix-leptos-grey?logo=matrix&labelColor=white&logoColor=black)](https://matrix.to/#/#leptos:matrix.org)
[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
@@ -24,8 +27,7 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
let increment = move |_| set_value.update(|value| *value += 1);
// create user interfaces with the declarative `view!` macro
view! {
cx,
view! { cx,
<div>
<button on:click=clear>"Clear"</button>
<button on:click=decrement>"-1"</button>
@@ -48,27 +50,27 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
## What does that mean?
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
## Learn more
Here are some resources for learning more about Leptos:
- [Book](https://leptos-rs.github.io/leptos/) (work in progress)
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
- Leptos Guide (in progress)
## `nightly` Note
Most of the examples assume youre using `nightly` Rust.
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 up your Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you havent already)
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you havent already):
```
rustup toolchain install nightly
@@ -76,17 +78,21 @@ rustup default nightly
rustup target add wasm32-unknown-unknown
```
If youre on `stable`, note the following:
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", 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.
```toml
[toolchain]
channel = "nightly"
targets = ["wasm32-unknown-unknown"]
```
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.
> 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`
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
```bash
cargo install cargo-leptos
@@ -95,17 +101,21 @@ cd [your project name]
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
Open browser to [http://localhost:3000/](http://localhost:3000/).
## FAQs
### Whats up with the name?
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
### Is it production ready?
People usually mean one of three things by this question.
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
With 0.1 the APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
The APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
2. **Are there bugs?**
@@ -115,7 +125,7 @@ Yes, Im sure there are. You can see from the state of our issue tracker over
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) dont have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if youre willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if youre willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
### Can I use this for native GUI?
@@ -133,8 +143,8 @@ I've put together a [very simple GTK example](https://github.com/leptos-rs/lepto
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. (Dioxus has made huge advances in performance with its recent 0.3 release, and is now roughly on par with Leptos.)
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. You can call functions, create timeouts, etc. within the body of your component functions because they wont be re-run. You dont need to think about manual dependency tracking for effects; fine-grained reactivity tracks dependencies automatically.
### How is this different from Sycamore?
@@ -142,9 +152,9 @@ Conceptually, these two frameworks are very similar: because both are built on f
There are some practical differences that make a significant difference:
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
```rust

View File

@@ -4,7 +4,8 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
l021 = { package = "leptos", version = "0.2.1" }
leptos = { path = "../leptos", features = ["ssr"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
tokio-test = "0.4"
@@ -16,15 +17,11 @@ lazy_static = "1"
log = "0.4"
strum = "0.24"
strum_macros = "0.24"
serde = { version = "1", features = ["derive", "rc"]}
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
tera = "1"
reactive-signals = "0.1.0-alpha.4"
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
"Document",
"HtmlElement",
"HtmlInputElement"
]
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]

View File

@@ -2,6 +2,6 @@
extern crate test;
//mod reactive;
mod ssr;
//åmod reactive;
//mod ssr;
mod todomvc;

View File

@@ -1,35 +1,113 @@
use std::{cell::Cell, rc::Rc};
use test::Bencher;
use std::{cell::Cell, rc::Rc};
#[bench]
fn leptos_create_1000_signals(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
fn leptos_deep_creation(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_deep_update(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_narrowing_down(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
fn leptos_fanning_out(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(|cx| {
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_narrowing_update(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
@@ -48,17 +126,20 @@ fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_scope, create_signal};
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope({
create_scope(runtime, {
let acc = Rc::clone(&acc);
move |cx| {
let (r, w) = create_signal(cx, 0);
@@ -76,16 +157,252 @@ fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
disposer.dispose();
}
});
runtime.dispose();
}
#[bench]
fn sycamore_create_1000_signals(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
fn rs_deep_update(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let sc = ClientRuntime::new_root_scope();
b.iter(|| {
let signal = signal!(sc, 0);
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(signal!(sc, move || prev.get() + 1))
} else {
memos.push(signal!(sc, move || signal.get() + 1))
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
});
}
#[bench]
fn rs_fanning_out(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let sig = signal!(cx, 0);
let memos = (0..1000)
.map(|_| signal!(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
});
}
#[bench]
fn rs_narrowing_update(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let memo = signal!(cx, {
let sigs = sigs.clone();
move || {
sigs.iter().map(|r| r.get()).sum::<i32>()
}
});
assert_eq!(memo.get(), 499500);
signal!(cx, {
let acc = Rc::clone(&acc);
move || {
acc.set(memo.get());
}
});
assert_eq!(acc.get(), 499500);
sigs[1].update(|n| *n += 1);
sigs[10].update(|n| *n += 1);
sigs[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo.get(), 499503);
});
}
#[bench]
fn l021_deep_creation(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_deep_update(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_narrowing_down(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_fanning_out(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_narrowing_update(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
}
});
assert_eq!(acc.get(), 499500);
writes[1].update(|n| *n += 1);
writes[10].update(|n| *n += 1);
writes[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo(), 499503);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope(runtime, {
let acc = Rc::clone(&acc);
move |cx| {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r());
}
});
w.update(|n| *n += 1);
}
})
})
.collect::<Vec<_>>();
for disposer in disposers {
disposer.dispose();
}
});
runtime.dispose();
}
#[bench]
fn sycamore_narrowing_down(b: &mut Bencher) {
use sycamore::reactive::{
create_effect, create_memo, create_scope, create_signal,
};
b.iter(|| {
let d = create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
let sigs = Rc::new(
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
);
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
@@ -97,13 +414,78 @@ fn sycamore_create_1000_signals(b: &mut Bencher) {
}
#[bench]
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
fn sycamore_fanning_out(b: &mut Bencher) {
use sycamore::reactive::{
create_effect, create_memo, create_scope, create_signal,
};
b.iter(|| {
let d = create_scope(|cx| {
let sig = create_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 1000);
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_deep_creation(b: &mut Bencher) {
use sycamore::reactive::*;
b.iter(|| {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));
}
}
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_deep_update(b: &mut Bencher) {
use sycamore::reactive::*;
b.iter(|| {
let d = create_scope(|cx| {
let signal = create_signal(cx, 0);
let mut memos = Vec::<&ReadSignal<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move || *prev.get() + 1));
} else {
memos.push(create_memo(cx, move || *signal.get() + 1));
}
}
signal.set(1);
assert_eq!(*memos[999].get(), 1001);
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_narrowing_update(b: &mut Bencher) {
use sycamore::reactive::{
create_effect, create_memo, create_scope, create_signal,
};
b.iter(|| {
let d = create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
let sigs = Rc::new(
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
);
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
@@ -129,7 +511,7 @@ fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
}
#[bench]
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
fn sycamore_scope_creation_and_disposal(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_scope, create_signal};
b.iter(|| {

View File

@@ -4,7 +4,7 @@ use test::Bencher;
fn leptos_ssr_bench(b: &mut Bencher) {
b.iter(|| {
use leptos::*;
HydrationCtx::reset_id();
leptos_dom::HydrationCtx::reset_id();
_ = create_scope(create_runtime(), |cx| {
#[component]
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
@@ -32,7 +32,8 @@ fn leptos_ssr_bench(b: &mut Bencher) {
assert_eq!(
rendered,
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here&#x27;s some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>"
);
});
});
}

View File

@@ -1,6 +1,7 @@
pub use leptos::*;
use miniserde::*;
use web_sys::HtmlInputElement;
use wasm_bindgen::JsCast;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Todos(pub Vec<Todo>);
@@ -110,10 +111,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
provide_context(cx, set_todos);
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener("hashchange", move |_| {
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
});
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
@@ -167,57 +164,79 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
});
view! { cx,
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" on:keydown=add_todo />
</header>
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
<input id="toggle-all" class="toggle-all" type="checkbox"
prop:checked={move || todos.with(|t| t.remaining() > 0)}
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
<For
each=filtered_todos
key=|todo| todo.id
view=move |todo: Todo| view! { cx, <Todo todo=todo.clone() /> }
/>
</ul>
</section>
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 {
" item"
} else {
" items"
}}
" left"
</span>
<ul class="filters">
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
</ul>
<button
class="clear-completed hidden"
class:hidden={move || todos.with(|t| t.completed() == 0)}
on:click=move |_| set_todos.update(|t| t.clear_completed())
>
"Clear completed"
</button>
</footer>
</section>
<footer class="info">
<p>"Double-click to edit a todo"</p>
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}.into_view(cx)
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus=""
on:keydown=add_todo
/>
</header>
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
prop:checked=move || todos.with(|t| t.remaining() > 0)
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
<For
each=filtered_todos
key=|todo| todo.id
view=move |cx, todo: Todo| {
view! { cx, <Todo todo=todo.clone()/> }
}
/>
</ul>
</section>
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
" left"
</span>
<ul class="filters">
<li>
<a
href="#/"
class="selected"
class:selected=move || mode() == Mode::All
>
"All"
</a>
</li>
<li>
<a href="#/active" class:selected=move || mode() == Mode::Active>
"Active"
</a>
</li>
<li>
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
"Completed"
</a>
</li>
</ul>
<button
class="clear-completed hidden"
class:hidden=move || todos.with(|t| t.completed() == 0)
on:click=move |_| set_todos.update(|t| t.clear_completed())
>
"Clear completed"
</button>
</footer>
</section>
<footer class="info">
<p>"Double-click to edit a todo"</p>
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}.into_view(cx)
}
#[component]
@@ -237,41 +256,36 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
};
view! { cx,
<li
class="todo"
class:editing={editing}
class:completed={move || (todo.completed)()}
//_ref=input
>
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
<div class="view">
<input
class="toggle"
type="checkbox"
prop:checked={move || (todo.completed)()}
/>
<label on:dblclick=move |_| set_editing(true)>
{move || todo.title.get()}
</label>
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
<button
class="destroy"
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
></button>
</div>
{move || editing().then(|| view! { cx,
<input
class="edit"
class:hidden={move || !(editing)()}
prop:value={move || todo.title.get()}
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup={move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {
set_editing(false);
{move || {
editing()
.then(|| {
view! { cx,
<input
class="edit"
class:hidden=move || !(editing)()
prop:value=move || todo.title.get()
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup=move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {
set_editing(false);
}
}
/>
}
}}
/>
})
}
})
}}
</li>
}
}

View File

@@ -7,19 +7,15 @@ mod yew;
#[bench]
fn leptos_todomvc_ssr(b: &mut Bencher) {
use ::leptos::*;
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::leptos::*;
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new(cx)/>
}
.into_view(cx)
.render_to_string(cx);
assert!(rendered.len() > 1);
let html = ::leptos::ssr::render_to_string(|cx| {
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
});
assert!(html.len() > 1);
});
}
@@ -57,21 +53,20 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
});
});
}
/*
#[bench]
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
b.iter(|| {
use self::leptos::*;
use ::leptos::*;
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
let html = ::leptos::ssr::render_to_string(|cx| {
view! {
cx,
<TodoMVC todos=Todos::new_with_1000(cx)/>
}.into_view(cx).render_to_string(cx);
assert!(rendered.len() > 1);
}
});
assert!(html.len() > 1);
});
}
@@ -108,5 +103,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
assert!(rendered.len() > 1);
});
});
}
*/
}

View File

@@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) {
let _ = TERA.render("template.html", &ctx).unwrap();
});
}
}

7
cargo-make/check.toml Normal file
View File

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

9
cargo-make/lint.toml Normal file
View File

@@ -0,0 +1,9 @@
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[tasks.check-style]
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../" }
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]

18
cargo-make/main.toml Normal file
View File

@@ -0,0 +1,18 @@
extend = [
{ path = "./check.toml" },
{ path = "./lint.toml" },
{ path = "./test.toml" },
]
[env]
RUSTFLAGS = ""
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"
[tasks.ci]
dependencies = ["lint", "test"]
[tasks.lint]
dependencies = ["check-format-flow"]

7
cargo-make/test.toml Normal file
View File

@@ -0,0 +1,7 @@
[tasks.test]
alias = "test-all"
[tasks.test-all]
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"

View File

@@ -28,6 +28,52 @@ let (a, set_a) = create_signal(cx, 0);
let b = move || a () > 5;
```
### Nested signal updates/reads triggering panic
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let resources = create_rw_signal(cx, HashMap::new());
let update = move |id: usize| {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(cx, 0))
.update(|amount| *amount += 1)
})
};
view! { cx,
<div>
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
<button on:click=move |_| update(1)>"+"</button>
</div>
}
}
```
Clicking the button twice will cause a panic, because of the nested signal *read*. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
You can fix this fairly easily by using the [`Scope::batch()`](https://docs.rs/leptos/latest/leptos/struct.Scope.html#method.batch) method:
```rust
let update = move |id: usize| {
cx.batch(move || {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(cx, 0))
.update(|amount| *amount += 1)
})
});
};
```
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
## Templates and the DOM
### `<input value=...>` doesn't update or stops updating
@@ -61,3 +107,19 @@ view! {
<input prop:value=a on:input=on_input />
}
```
## Build configuration
### Cargo feature resolution in workspaces
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
```toml
[workspace]
members = ["member1", "member2"]
resolver = "2"
```

View File

@@ -1 +1 @@
book
book

14
docs/book/README.md Normal file
View File

@@ -0,0 +1,14 @@
This project contains the core of a new introductory guide to Leptos.
It is built using `mdbook`. You can view a local copy by installing `mdbook`
```bash
cargo install mdbook
```
and run the book with
```
mdbook serve
```
It should be available at `http://localhost:3000`.

View File

@@ -1,16 +1,2 @@
[book]
authors = ["Greg Johnston"]
language = "en"
multilingual = false
src = "src"
title = "The Leptos Guide"
[preprocessor]
[preprocessor.mermaid]
command = "mdbook-mermaid"
[output]
[output.html]
additional-js = ["mermaid.min.js", "mermaid-init.js"]
[output.html.playground]
runnable = false

View File

@@ -1 +0,0 @@
mermaid.initialize({startOnLoad:true});

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
[package]
name = "ch02_getting_started"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Leptos • Todos</title>
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
<link data-trunk rel="rust" data-wasm-opt="z" />
</head>
<body></body>
</html>

View File

@@ -1,5 +0,0 @@
use leptos::*;
fn main() {
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
}

View File

@@ -1,7 +0,0 @@
[package]
name = "ch03_building_ui"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Leptos • Todos</title>
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
<link data-trunk rel="rust" data-wasm-opt="z" />
</head>
<body></body>
</html>

View File

@@ -1,39 +0,0 @@
use leptos::*;
fn main() {
mount_to_body(|cx| {
let name = "gbj";
let userid = 0;
let _input_element: Element;
view! {
cx,
<main>
<h1>"My Tasks"</h1> // text nodes are wrapped in quotation marks
<h2>"by " {name}</h2>
<input
type="text" // attributes work just like they do in HTML
name="new-todo"
prop:value="todo" // `prop:` lets you set a property on a DOM node
value="initial" // side note: the DOM `value` attribute only sets *initial* value
// this is very important when working with forms!
_ref=_input_element // `_ref` stores tis element in a variable
/>
<ul data-user=userid> // attributes can take expressions as values
<li class="todo my-todo" // here we set the `class` attribute
class:completed=true // `class:` also lets you toggle individual classes
on:click=|_| todo!() // `on:` adds an event listener
>
"Buy milk."
</li>
<li class="todo my-todo" class:completed=false>
"???"
</li>
<li class="todo my-todo" class:completed=false>
"Profit!!!"
</li>
</ul>
</main>
}
})
}

View File

@@ -1,7 +0,0 @@
[package]
name = "ch04_reactivity"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"

View File

@@ -1,28 +0,0 @@
use leptos::*;
fn main() {
run_scope(create_runtime(), |cx| {
// signal
let (count, set_count) = create_signal(cx, 1);
// derived signal
let double_count = move || count() * 2;
// memo
let memoized_square = create_memo(cx, move |_| count() * count());
// effect
create_effect(cx, move |_| {
println!(
"count =\t\t{} \ndouble_count = \t{}, \nsquare = \t{}",
count(),
double_count(),
memoized_square()
);
});
set_count(1);
set_count(2);
set_count(3);
});
}

View File

@@ -1,10 +1,19 @@
# Introduction
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework. Together, well build a simple todo app—first as a client-side app, then as a full-stack app.
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
It will walk through the fundamental concepts you need to build applications,
beginning with a simple application rendered in the browser, and building toward a
full-stack application with server-side rendering and hydration.
The guide doesnt assume you know anything about fine-grained reactivity or the details of modern Web frameworks. It does assume you are familiar with the Rust programming language, HTML, CSS, and the DOM and other Web APIs.
The guide doesnt assume you know anything about fine-grained reactivity or the
details of modern Web frameworks. It does assume you are familiar with the Rust
programming language, HTML, CSS, and the DOM and basic Web APIs.
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript) and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities to other frameworks like React (JavaScript), Yew (Rust), and Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to understand Leptos.
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
understand Leptos.
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).

View File

@@ -1,37 +1,84 @@
# Getting Started
> The code for this chapter can be found [here](https://github.com/leptos-rs/leptos/tree/main/docs/book/project/ch02_getting_started).
There are two basic paths to getting started with Leptos:
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/leptos-rs/leptos/tree/main/examples) do. (Trunk is a simple build tool that includes a dev server.)
1. Client-side rendering with [Trunk](https://trunkrs.dev/)
2. Full-stack rendering with [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos)
For the early examples, it will be easiest to begin with Trunk. Well introduce
`cargo-leptos` a little later in this series.
If you dont already have it installed, you can install Trunk by running
```bash
cargo install --lock trunk
cargo install trunk
```
Create a basic Rust binary project
Create a basic Rust project
```bash
cargo init leptos-todo
cargo init leptos-tutorial
```
Add `leptos` as a dependency to your `Cargo.toml` with the `csr` featured enabled. (That stands for “client-side rendering.” Well talk more about Leptoss support for server-side rendering and hydration later.)
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```toml
leptos = "0.0"
```bash
cargo add leptos --features=csr,nightly
```
Youll want to set up a basic `index.html` with the following content:
Or you can leave off `nighly` if you're using stable Rust
```bash
cargo add leptos --features=csr
```
> Using `nightly` Rust, and the `nightly` feature in Leptos enables the function-call syntax for signal getters and setters that is used in most of this book.
>
> To use `nightly` Rust, you can run
>
> ```bash
> rustup toolchain install nightly
> rustup default nightly
> ```
>
> If youd rather use stable Rust with Leptos, you can do that too. In the guide and examples, youll just use the [`ReadSignal::get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) and [`WriteSignal::set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) methods instead of calling signal getters and setters as functions.
Make sure you've added the `wasm32-unknown-unknown` target so that Rust can compile your code to WebAssembly to run in the browser.
```bash
rustup target add wasm32-unknown-unknown
```
Create a simple `index.html` in the root of the `leptos-tutorial` directory
```html
{{#include ../project/ch02_getting_started/index.html}}
<!DOCTYPE html>
<html>
<head></head>
<body></body>
</html>
```
Lets start with a very simple `main.rs`
And add a simple “Hello, world!” to your `main.rs`
```rust
{{#include ../project/ch02_getting_started/src/main.rs}}
use leptos::*;
fn main() {
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
}
```
Now run `trunk serve --open`. Trunk should automatically compile your app and open it in your default browser. If you make edits to `main.rs`, Trunk will recompile your source code and live-reload the page.
Your directory structure should now look something like this
```
leptos_tutorial
├── src
│ └── main.rs
├── Cargo.toml
├── index.html
```
Now run `trunk serve --open` from the root of the `leptos-tutorial` directory.
Trunk should automatically compile your app and open it in your default browser.
If you make edits to `main.rs`, Trunk will recompile your source code and
live-reload the page.

View File

@@ -1,49 +0,0 @@
# Templating: Building User Interfaces
> The code for this chapter can be found [here](https://github.com/leptos-rs/leptos/tree/main/docs/book/project/ch03_building_ui).
## RSX and the `view!` macro
Okay, that “Hello, world!” was a little boring. Were going to be building a todo app, so lets look at something a little more complicated.
As you noticed in the first example, Leptos lets you describe your user interface with a declarative `view!` macro. It looks something like this:
```
view! {
cx, // this is the "reactive scope": more on that in the next chapter
<p>"..."</p> // this is some HTML-ish stuff
}
```
The “HTML-ish stuff” is what we call “RSX”: XML in Rust. (You may recognize the similarity to JSX, which is the mixed JavaScript/XML syntax used by frameworks like React.)
Heres a more in-depth example:
```rust
{{#include ../project/ch03_building_ui/src/main.rs}}
```
Youll probably notice a few things right away:
1. Elements without children need to be explicit closed with a `/` (`<input/>`, not `<input>`)
2. Text nodes are formatted as strings, i.e., wrapped in quotation marks (`"My Tasks"`)
3. Dynamic blocks can be inserted as children of elements, if wrapped in curly braces (`<h2>"by " {name}</h2>`)
4. Attributes can be given Rust expressions as values. This could be a string literal as in HTML (`<input type="text" .../>)` or a variable or block (`data-user=userid` or `on:click=move |_| { ... }`)
5. Unlike in HTML, whitespace is ignored and should be manually added (its `<h2>"by " {name}</h2>`, not `<h2>"by" {name}</h2>`; the space between `"by"` and `{name}` is ignored.)
6. Normal attributes work exactly like you'd think they would.
7. There are also special, prefixed attributes.
- `class:` lets you make targeted updates to a single class
- `on:` lets you add an event listener
- `prop:` lets you set a property on a DOM element
- `_ref` stores the DOM element youre creating in a variable
> You can find more information in the [reference docs for the `view!` macro](https://docs.rs/leptos/0.0.15/leptos/macro.view.html).
## But, wait...
This example shows some parts of the Leptos templating syntax. But its completely static.
How do you actually make the user interface interactive?
In the next chapter, well talk about “fine-grained reactivity,” which is the core of the Leptos framework.

View File

@@ -1,240 +0,0 @@
# Reactivity
## What is reactivity?
A few months ago, I completely baffled a friend by trying to explain what I was working on. “You have two variables, right? Call them `a` and `b`. And then you have a third variable, `c`. And when you update `a` or `b`, the value of `c` just _automatically changes_. And it changes _on the screen_! Automatically!”
“Isnt that just... how computers work?” she asked me, puzzled. If your programming experience is limited to something like spreadsheets, its a reasonable enough assumption. This is, after all, how math works.
But you know this isn't how ordinary imperative programming works.
```rust,should_panic
let mut a = 0;
let mut b = 0;
let c = a + b;
assert_eq!(c, 0); // sanity check
a = 2;
b = 2;
// now c = 4, right?
assert_eq!(c, 4); // nope. we all know this is wrong!
```
But thats _exactly_ how reactive programming works.
```rust
use leptos::*;
run_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();
assert_eq!(c(), 0); // yep, still true
set_a(2);
set_b(2);
assert_eq!(c(), 4); // ohhhhh yeah.
});
```
Hopefully, this makes some intuitive sense. After all, `c` is a closure. Calling it again causes it to access its values a second time. This isnt _that_ cool.
```rust
use leptos::*;
run_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();
create_effect(cx, move |_| {
println!("c = {}", c()); // prints "c = 0"
});
set_a(2); // prints "c = 2"
set_b(2); // prints "c = 4"
});
```
This examples a little different. [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) defines a “side effect,” a bridge between the reactive system of signals and the outside world. Effects synchronize the reactive system with everything else: the console, the filesystem, an HTTP request, whatever.
Because the closure `c` is called within the effect and in turns calls the signals `a` and `b`, the effect automatically subscribes to the signals `a` and `b`. This means that whenever `a` or `b` is updated, the effect will re-run, logging the value again.
You can picture the reactive graph for this system like this:
```mermaid
graph TD;
A-->C;
B-->C;
C-->Effect;
```
This is the foundation on which _everything_ else is built.
## Reactive Primitives
### Overview
The reactive system is built on the interaction between these two halves: **signals** and **effects**. When a signal is called inside an effect, the effect automatically subscribes to the signal. When a signals value is updated, it automatically notifies all its subscribers, and they re-run.
The following simple example contains most of the core reactive concepts:
```rust
{{#include ../project/ch04_reactivity/src/main.rs}}
```
This creates a reactive graph like this:
```mermaid
graph TD;
count-->double_count;
count-->memoized_square;
count-->effect;
double_count-->effect;
memoized_square-->effect;
```
**Signals** are reactive values created using [`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html) or [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html).
**Derived Signals** computations in ordinary closures that rely on other signals. The computation re-runs whenever you access its value.
**Memos** are computations that are memoized with [create_memo](https://docs.rs/leptos/latest/leptos/fn.create_memo.html). Memos only re-run when one of their signal dependencies has changed.
And **effects** (created with [create_effect](<(https://docs.rs/leptos/latest/leptos/fn.create_effect.html)>) synchronize the reactive system with something outside it.
The rest of this chapter will walk through each of these concepts in more depth.
### Signals
A **signal** is a piece of data that may change over time, and notifies other code when it has changed. This is the core primitive of Leptoss reactive system.
Creating a signal is very simple. You call `create_signal`, passing in the reactive scope and the default value, and receive a tuple containing a `ReadSignal` and a `WriteSignal`.
```rust
let (value, set_value) = create_signal(cx, 0);
```
> If youve used signals in Sycamore or Solid, observables in MobX or Knockout, or a similar primitive in reactive library, you probably have a pretty good idea of how signals work in Leptos. If youre familiar with React, Yew, or Dioxus, you may recognize a similar pattern to their `use_state` hooks.
#### `ReadSignal<T>`
The [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) half of this tuple allows you to get the current value of the signal. Reading that value in a reactive context automatically subscribes to any further changes. You can access the value by simply calling the `ReadSignal` as a function.
```rust
let (value, set_value) = create_signal(cx, 0);
// calling value() with return the current value of the signal,
// and automatically track changes if you're in a reactive context
assert_eq!(value(), 0);
```
> Here, a **reactive context** means anywhere within an `Effect`. Leptoss templating system is built on top of its reactive system, so if youre reading the signals value within the template, the template will automatically subscribe to the signal and update exactly the value that needs to change in the DOM.
Calling a `ReadSignal` clones the value it contains. If thats too expensive, use [`ReadSignal::with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#method.with) to borrow the value and do whatever you need.
```rust
struct MySuperExpensiveStruct {
a: String,
b: StructThatsSuperExpensiveToClone
}
let (value, set_value) = create_signal(cx, MySuperExpensiveStruct::default());
// ❌ this is going to clone the `StructThatsSuperExpensiveToClone` unnecessarily!
let lowercased = move || value().a.to_lowercase();
// ✅ only use what we need
let lowercased = move || value.with(|value: &MySuperExpensiveStruct| value.a.to_lowercase());
```
#### `WriteSignal<T>`
The [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) half of this tuple allows you to update the value of the signal, which will automatically notify anything thats listening to the value that something has changed. If you simply call the `WriteSignal` as a function, its value will be set to the argument you pass. If you want to mutate the value in place instead of replacing it, you can call [`WriteSignal::update`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#method.update) instead.
```rust
// often you just want to replace the value
let (value, set_value) = create_signal(cx, 0);
set_value(1);
assert_eq!(value(), 1);
// sometimes you want to mutate something in place, like a Vec. Just call update()
let (items, set_items) = create_signal(cx, vec![0]);
set_items.update(|items: &mut Vec<i32>| items.push(1));
assert_eq!(items(), vec![1]);
```
> Under the hood, `set_value(1)` is just syntactic sugar for `set_value.update(|n| *n = 1)`.
#### `RwSignal<T>`
This kind of “read-write segregation,” in which the getter and the setter are stored in separate variables, may be familiar from the tuple-based ”hooks” pattern in libraries like React, Solid, Yew, or Dioxus. It encourages clear contracts between components. For example, if a child component only needs to be able to read a signal, but shouldnt be able to update it (and therefore trigger changes in other parts of the application), you can pass it only the `ReadSignal`.
Sometimes, however, you may prefer to keep the getter and setter combined in one variable. For example, its awkward and repetitive to store both halves of a signal in another data structure:
```rust
# use leptos::*;
// pretty repetitive
struct AppState {
count: ReadSignal<i32>,
set_count: WriteSignal<i32>,
name: ReadSignal<String>,
set_name: WriteSignal<String>
}
#[component]
fn App(cx: Scope) {
let (count, set_count) = create_signal(cx, 0);
let (name, set_name) = create_signal(cx, "Alice".to_string());
provide_context(cx, AppState {
count,
set_count,
name,
set_name
})
todo!()
}
```
Or maybe you just like to keep your getters and setters in one place.
In this case, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) and the [`RwSignal`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html) type. This returns a **R**ead-**w**rite Signal, which has the same [`get`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get), [`with`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.with), [`set`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.set), and [`update`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.update) functions as the `ReadSignal` and `WriteSignal` halves.
```rust
# use leptos::*;
// better
struct AppState {
count: RwSignal<i32>,
name: RwSignal<String>,
}
#[component]
fn App(cx: Scope) {
let count = create_rw_signal(cx, 0);
let name = create_rw_signal(cx, "Alice".to_string());
provide_context(cx, AppState {
count,
name,
})
todo!()
}
```
If you still want to hand off read-only access to another part of the app, you can get a `ReadSignal` with [`RwSignal::read_only()`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get).
### Derived Signals
(todo)
### Memos
(todo)
### Effects
(todo)

View File

@@ -0,0 +1,185 @@
# Global State Management
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
1. Using the router to drive global state via the URL
2. Passing signals through context
3. Creating a global state struct and creating lenses into it with `create_slice`
## Option #1: URL as Global State
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 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.
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`.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
// here we create a signal in the root that can be consumed
// anywhere in the app.
let (count, set_count) = create_signal(cx, 0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(cx, count);
view! { cx,
// SetterButton is allowed to modify the count
<SetterButton set_count/>
// These consumers can only read from it
// But we could give them write access by passing `set_count` if we wanted
<FancyMath/>
<ListItems/>
}
}
```
`<SetterButton/>` is the kind of counter weve written several times now.
(See the sandbox below if you dont understand what I mean.)
`<FancyMath/>` and `<ListItems/>` both consume the signal were providing via
`use_context` and do something with it.
```rust
/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath(cx: Scope) -> impl IntoView {
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>(cx)
// we know we just provided this in the parent component
.expect("there to be a `count` signal provided");
let is_even = move || count() & 1 == 0;
view! { cx,
<div class="consumer blue">
"The number "
<strong>{count}</strong>
{move || if is_even() {
" is"
} else {
" is not"
}}
" even."
</div>
}
}
```
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(Copy, Clone, Debug)]
struct GlobalState {
count: RwSignal<i32>,
name: RwSignal<String>
}
impl GlobalState {
pub fn new(cx: Scope) -> Self {
Self {
count: create_rw_signal(cx, 0),
name: create_rw_signal(cx, "Bob".to_string())
}
}
}
#[component]
fn App(cx: Scope) -> impl IntoView {
provide_context(cx, GlobalState::new(cx));
// etc.
}
```
## 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 = expect_context::<RwSignal<GlobalState>>(cx);
// `create_slice` lets us create a "lens" into the data
let (count, set_count) = create_slice(
cx,
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data
|state| state.count,
// our setter describes how to mutate that slice, given a new value
|state, n| state.count = n,
);
view! { cx,
<div class="consumer blue">
<button
on:click=move |_| {
set_count(count() + 1);
}
>
"Increment Global Count"
</button>
<br/>
<span>"Count is: " {count}</span>
</div>
}
}
```
Clicking this button only updates `state.count`, so if we create another slice
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>

View File

@@ -2,5 +2,46 @@
- [Introduction](./01_introduction.md)
- [Getting Started](./02_getting_started.md)
- [Templating: Building User Interfaces](./03_building_ui.md)
- [Reactivity: Making Things Interactive](./04_reactivity.md)
- [Building User Interfaces](./view/README.md)
- [A Basic Component](./view/01_basic_component.md)
- [Dynamic Attributes](./view/02_dynamic_attributes.md)
- [Components and Props](./view/03_components.md)
- [Iteration](./view/04_iteration.md)
- [Forms and Inputs](./view/05_forms.md)
- [Control Flow](./view/06_control_flow.md)
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.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)
- [Suspense](./async/11_suspense.md)
- [Transition](./async/12_transition.md)
- [Actions](./async/13_actions.md)
- [Interlude: Projecting Children](./interlude_projecting_children.md)
- [Global State Management](./15_global_state.md)
- [Router](./router/README.md)
- [Defining `<Routes/>`](./router/16_routes.md)
- [Nested Routing](./router/17_nested_routing.md)
- [Params and Queries](./router/18_params_and_queries.md)
- [`<A/>`](./router/19_a.md)
- [`<Form/>`](./router/20_form.md)
- [Interlude: Styling](./interlude_styling.md)
- [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 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

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

View File

@@ -0,0 +1,55 @@
# Loading Data with Resources
A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if its still pending.
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments (other than the ubiquitous `cx`):
1. a source signal, which will generate a new `Future` whenever it changes
2. a fetcher function, which takes the data from that signal and returns a `Future`
Heres an example
```rust
// our source signal: some synchronous, local state
let (count, set_count) = create_signal(cx, 0);
// our resource
let async_data = create_resource(cx,
count,
// every time `count` changes, this will run
|value| async move {
log!("loading data from API");
load_data(value).await
},
);
```
To create a resource that simply runs once, you can pass a non-reactive, empty source signal:
```rust
let once = create_resource(cx, || (), |_| async move { load_data().await });
```
To access the value you can use `.read(cx)` or `.with(cx, |data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but with two differences
1. For any `Resource<_, T>`, they always return `Option<T>`, not `T`: because its always possible that your resource is still loading.
2. They take a `Scope` argument. Youll see why in the next chapter, on `<Suspense/>`.
So, you can show the current state of a resource in your view:
```rust
let once = create_resource(cx, || (), |_| async move { load_data().await });
view! { cx,
<h1>"My Data"</h1>
{move || match once.read(cx) {
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
}}
}
```
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,74 @@
# `<Suspense/>`
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
```rust
let (count, set_count) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
view! { cx,
<h1>"My Data"</h1>
{move || match once.read(cx) {
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
}}
}
```
But what if we have two resources, and want to wait for both of them?
```rust
let (count, set_count) = create_signal(cx, 0);
let (count2, set_count2) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
view! { cx,
<h1>"My Data"</h1>
{move || match (a.read(cx), b.read(cx)) {
(Some(a), Some(b)) => view! { cx,
<ShowA a/>
<ShowA b/>
}.into_view(cx),
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
}}
}
```
Thats not _so_ bad, but its kind of annoying. What if we could invert the flow of control?
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If its still waiting for resources to load, it shows the `fallback`. When theyve all loaded, it shows the children.
```rust
let (count, set_count) = create_signal(cx, 0);
let (count2, set_count2) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
view! { cx,
<h1>"My Data"</h1>
<Suspense
fallback=move || view! { cx, <p>"Loading..."</p> }
>
<h2>"My Data"</h2>
<h3>"A"</h3>
{move || {
a.read(cx)
.map(|a| view! { cx, <ShowA a/> })
}}
<h3>"B"</h3>
{move || {
b.read(cx)
.map(|b| view! { cx, <ShowB b/> })
}}
</Suspense>
}
```
Every time one of the resources is reloading, the `"Loading..."` fallback will show again.
This inversion of the flow of control makes it easier to add or remove individual resources, as you dont need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which well talk about during a later chapter.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,11 @@
# `<Transition/>`
Youll notice in the `<Suspense/>` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, theres [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html).
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,96 @@
# Mutating Data with Actions
Weve talked about how to load `async` data with resources. Resources immediately load data and work closely with `<Suspense/>` and `<Transition/>` components to show whether data is loading in your app. But what if you just want to call some arbitrary `async` function and keep track of what its doing?
Well, you could always use [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html). This allows you to just spawn an `async` task in a synchronous environment by handing the `Future` off to the browser (or, on the server, Tokio or whatever other runtime youre using). But how do you know if its still pending? Well, you could just set a signal to show whether its loading, and another one to show the result...
All of this is true. Or you could use the final `async` primitive: [`create_action`](https://docs.rs/leptos/latest/leptos/fn.create_action.html).
Actions and resources seem similar, but they represent fundamentally different things. If youre trying to load data by running an `async` function, either once or when some other value changes, you probably want to use `create_resource`. If youre trying to occasionally run an `async` function in response to something like a user clicking a button, you probably want to use `create_action`.
Say we have some `async` function we want to run.
```rust
async fn add_todo_request(new_title: &str) -> Uuid {
/* do some stuff on the server to add a new todo */
}
```
`create_action` takes a reactive `Scope` and an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
> The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
>
> ```rust
> // if there's a single argument, just use that
> let action1 = create_action(cx, |input: &String| {
> let input = input.clone();
> async move { todo!() }
> });
>
> // if there are no arguments, use the unit type `()`
> let action2 = create_action(cx, |input: &()| async { todo!() });
>
> // if there are multiple arguments, use a tuple
> let action3 = create_action(cx,
> |input: &(usize, String)| async { todo!() }
> );
> ```
>
> Because the action function takes a reference but the `Future` needs to have a `'static` lifetime, youll usually need to clone the value to pass it into the `Future`. This is admittedly awkward but it unlocks some powerful features like optimistic UI. Well see a little more about that in future chapters.
So in this case, all we need to do to create an action is
```rust
let add_todo_action = create_action(cx, |input: &String| {
let input = input.to_owned();
async move { add_todo_request(&input).await }
});
```
Rather than calling `add_todo_action` directly, well call it with `.dispatch()`, as in
```rust
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.
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_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.
```rust
let input_ref = create_node_ref::<Input>(cx);
view! { cx,
<form
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
let input = input_ref.get().expect("input to exist");
add_todo_action.dispatch(input.value());
}
>
<label>
"What do you need to do?"
<input type="text"
node_ref=input_ref
/>
</label>
<button type="submit">"Add Todo"</button>
</form>
// use our loading state
<p>{move || pending().then("Loading...")}</p>
}
```
Now, theres a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, youll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Dont worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,9 @@
# Working with `async`
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.
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.
In this chapter, well see how Leptos helps smooth out that process for you.

View File

@@ -0,0 +1,177 @@
# Projecting Children
As you build components you may occasionally find yourself wanting to “project” children through multiple layers of components.
## The Problem
Consider the following:
```rust
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
view! { cx,
<Suspense
fallback=|| ()
>
<Show
// check whether user is verified
// by reading from the resource
when=move || todo!()
fallback=fallback
>
{children(cx)}
</Show>
</Suspense>
}
}
```
This is pretty straightforward: when the user is logged in, we want to show `children`. 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 `<LoggedIn/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
This wont compile.
```
error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` closure
error[E0507]: cannot move out of `children`, a captured variable in an `Fn` closure
```
The problem here is that both `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple times. The first time you construct `<Suspense/>`s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` children construction.
## The Details
> Feel free to skip ahead to the solution.
If you want to really understand the issue here, it may help to look at the expanded `view` macro. Heres a cleaned-up version:
```rust
Suspense(
cx,
::leptos::component_props_builder(&Suspense)
.fallback(|| ())
.children({
// fallback and children are moved into this closure
Box::new(move |cx| {
{
// fallback and children captured here
leptos::Fragment::lazy(|| {
vec![
(Show(
cx,
::leptos::component_props_builder(&Show)
.when(|| true)
// but fallback is moved into Show here
.fallback(fallback)
// and children is moved into Show here
.children(children)
.build(),
)
.into_view(cx)),
]
})
}
})
})
.build(),
)
```
All components own their props; so the `<Show/>` in this case cant be called because it only has captured references to `fallback` and `children`.
## Solution
However, both `<Suspense/>` and `<Show/>` take `ChildrenFn`, i.e., their `children` should implement the `Fn` type so they can be called multiple times with only an immutable reference. This means we dont need to own `children` or `fallback`; we just need to be able to pass `'static` references to them.
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods.
In this case, its really simple:
```rust
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
let fallback = store_value(cx, fallback);
let children = store_value(cx, children);
view! { cx,
<Suspense
fallback=|| ()
>
<Show
when=|| todo!()
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
>
{children.with_value(|children| children(cx))}
</Show>
</Suspense>
}
}
```
At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `<Show/>` component and call them there.
## A Final Note
Note that this works because `<Show/>` and `<Suspense/>` only need an immutable reference to their children (which `.with_value` can give it), not ownership.
In other cases, you may need to project owned props through a function that takes `ChildrenFn` and therefore needs to be called more than once. In this case, you may find the `clone:` helper in the`view` macro helpful.
Consider this example
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let name = "Alice".to_string();
view! { cx,
<Outer>
<Inner>
<Inmost name=name.clone()/>
</Inner>
</Outer>
}
}
#[component]
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
children(cx)
}
#[component]
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
children(cx)
}
#[component]
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
view! { cx,
<p>{name}</p>
}
}
```
Even with `name=name.clone()`, this gives the error
```
cannot move out of `name`, a captured variable in an `Fn` closure
```
Its captured through multiple levels of children that need to run more than once, and theres no obvious way to clone it _into_ the children.
In this case, the `clone:` syntax comes in handy. Calling `clone:name` will clone `name` _before_ moving it into `<Inner/>`s children, which solves our ownership issue.
```rust
view! { cx,
<Outer>
<Inner clone:name>
<Inmost name=name.clone()/>
</Inner>
</Outer>
}
```
These issues can be a little tricky to understand or debug, because of the opacity of the `view` macro. But in general, they can always be solved.

View File

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

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,76 @@
# Interlude: Reactivity and Functions
One of our core contributors said to me recently: “I never used closures this often
until I started using Leptos.” And its true. Closures are at the heart of any Leptos
application. It sometimes looks a little silly:
```rust
// a signal holds a value, and can be updated
let (count, set_count) = create_signal(cx, 0);
// a derived signal is a function that accesses other signals
let double_count = move || count() * 2;
let count_is_odd = move || count() & 1 == 1;
let text = move || if count_is_odd() {
"odd"
} else {
"even"
};
// an effect automatically tracks the signals it depends on
// and reruns when they change
create_effect(cx, move |_| {
log!("text = {}", text());
});
view! { cx,
<p>{move || text().to_uppercase()}</p>
}
```
Closures, closures everywhere!
But why?
## Functions and UI Frameworks
Functions are at the heart of every UI framework. And this makes perfect sense. Creating a user interface is basically divided into two phases:
1. initial rendering
2. updates
In a web framework, the framework does some kind of initial rendering. Then it hands control back over to the browser. When certain events fire (like a mouse click) or asynchronous tasks finish (like an HTTP request finishing), the browser wakes the framework back up to update something. The framework runs some kind of code to update your user interface, and goes back asleep until the browser wakes it up again.
The key phrase here is “runs some kind of code.” The natural way to “run some kind of code” at an arbitrary point in time—in Rust or in any other programming language—is to call a function. And in fact every UI framework is based on rerunning some kind of function over and over:
1. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM
2. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the components state
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that rerun
Thats what all our components are doing.
Take our typical `<SimpleCounter/>` example in its simplest form:
```rust
#[component]
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
let increment = move |_| set_value.update(|value| *value += 1);
view! { cx,
<button on:click=increment>
{value}
</button>
}
}
```
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in responsive to a change.
So remember two things:
1. Your component function is a setup function, not a render function: it only runs once.
2. For values in your view template to be reactive, they must be functions: either signals (which implement the `Fn` traits) or closures.

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

@@ -0,0 +1,101 @@
# Defining Routes
## Getting Started
Its easy to get started with the router.
First things first, make sure youve added the `leptos_router` package to your dependencies.
> Its important that the router is a separate package from `leptos` itself. This means that everything in the router can be defined in user-land code. If you want to create your own router, or use no router, youre completely free to do that!
And import the relevant types from the router, either with something like
```rust
use leptos_router::{Route, RouteProps, Router, RouterProps, Routes, RoutesProps};
```
or simply
```rust
use leptos_router::*;
```
## Providing the `<Router/>`
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
> You shouldnt try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
Lets start with a simple `<App/>` component using the router:
```rust
use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<nav>
/* ... */
</nav>
<main>
/* ... */
</main>
</Router>
}
}
```
## Defining `<Routes/>`
The [`<Routes/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Route.html) component.
You should place the `<Routes/>` component at the location within your app where you want routes to be rendered. Everything outside `<Routes/>` will be present on every page, so you can leave things like a navigation bar or menu outside the `<Routes/>`.
```rust
use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<nav>
/* ... */
</nav>
<main>
// all our routes will appear inside <main>
<Routes>
/* ... */
</Routes>
</main>
</Router>
}
}
```
Individual routes are defined by providing children to `<Routes/>` with the `<Route/>` component. `<Route/>` takes a `path` and a `view`. When the current location matches `path`, the `view` will be created and displayed.
The `path` can include
- a static path (`/users`),
- dynamic, named parameters beginning with a colon (`/:id`),
- and/or a wildcard beginning with an asterisk (`/user/*any`)
The `view` is a function that takes a `Scope` and returns a view.
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home/> }/>
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
</Routes>
```
> The router scores each route to see how good a match it is, so you can define your routes in any order.
Now if you navigate to `/` or to `/users` youll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` youll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
Simple enough?

View File

@@ -0,0 +1,172 @@
# Nested Routing
We just defined the following set of routes:
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home /> }/>
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
</Routes>
```
Theres a certain amount of duplication here: `/users` and `/users/:id`. This is fine for a small app, but you can probably already tell it wont scale well. Wouldnt it be nice if we could nest these routes?
Well... you can!
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home /> }/>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
</Route>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
</Routes>
```
But wait. Weve just subtly changed what our application does.
The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if theres anything you dont understand.
# Nested Routes as Layout
Nested routes are a form of layout, not a method of route definition.
Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple `<Route/>`s on the page at the same time, side by side.
Lets look back at our practical example.
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
</Routes>
```
This means:
- If I go to `/users`, I get the `<Users/>` component.
- If I go to `/users/3`, I get the `<UserProfile/>` component (with the parameter `id` set to `3`; more on that later)
Lets say I use nested routes instead:
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
</Route>
</Routes>
```
This means:
- If I go to `/users/3`, the path matches two `<Route/>`s: `<Users/>` and `<UserProfile/>`.
- If I go to `/users`, the path is not matched.
I actually need to add a fallback route
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="" view=|cx| view! { cx, <NoUser /> }/>
</Route>
</Routes>
```
Now:
- If I go to `/users/3`, the path matches `<Users/>` and `<UserProfile/>`.
- If I go to `/users`, the path matches `<Users/>` and `<NoUser/>`.
When I use nested routes, in other words, each **path** can match multiple **routes**: each URL can render the views provided by multiple `<Route/>` components, at the same time, on the same page.
This may be counter-intuitive, but its very powerful, for reasons youll hopefully see in a few minutes.
## Why Nested Routing?
Why bother with this?
Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like `/contacts/greg`, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If theres no contact selected, maybe you want to show a little instructional text.
You can easily define this with nested routes
```rust
<Routes>
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }/>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
}/>
</Route>
</Routes>
```
You can go even deeper. Say you want to have tabs for each contacts address, email/phone, and your conversations with them. You can add _another_ set of nested routes inside `:id`:
```rust
<Routes>
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
<Route path="address" view=|cx| view! { cx, <Address/> }/>
<Route path="messages" view=|cx| view! { cx, <Messages/> }/>
</Route>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
}/>
</Route>
</Routes>
```
> The main page of the [Remix website](https://remix.run/), a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice.
## `<Outlet/>`
Parent routes do not automatically render their nested routes. After all, they are just components; they dont know exactly where they should render their children, and “just stick it at the end of the parent component” is not a great answer.
Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things:
- if there is no nested route that has been matched, it shows nothing
- if there is a nested route that has been matched, it shows its `view`
Thats all! But its important to know and to remember, because its a common source of “Why isnt this working?” frustration. If you dont provide an `<Outlet/>`, the nested route wont be displayed.
```rust
#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
let contacts = todo!();
view! { cx,
<div style="display: flex">
// the contact list
<For each=contacts
key=|contact| contact.id
view=|cx, contact| todo!()
>
// the nested child, if any
// dont forget this!
<Outlet/>
</div>
}
}
```
## Nested Routing and Performance
All of this is nice, conceptually, but again—whats the big deal?
Performance.
In a fine-grained reactive library like Leptos, its always important to do the least amount of rendering work you can. Because were working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy.
Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the `<ContactList/>` should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of `<ContactList/>`, navigating from Greg to Alice to Bob wont clear the search.
In fact, in this case, we dont even need to rerender the `<Contact/>` component when moving between contacts. The router will just reactively update the `:id` parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, well update single text nodes to change the contacts name, address, and so on, without doing _any_ additional rerendering.
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple well cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so dont be surprised if theres anything you dont understand.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,79 @@
# Params and Queries
Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point.
There are two ways you can do this:
1. named route **params** like `id` in `/users/:id`
2. named route **queries** like `q` in `/search?q=Foo`
Because of the way URLs are built, you can access the query from _any_ `<Route/>` view. You can access route params from the `<Route/>` that defines them or any of its nested children.
Accessing params and queries is pretty simple with a couple of hooks:
- [`use_query`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
- [`use_params`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
Each of these comes with a typed option (`use_query` and `use_params`) and an untyped option (`use_query_map` and `use_params_map`).
The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.2.3/leptos_router/trait.Params.html) trait on a struct.
> `Params` is a very lightweight trait to convert a flat key-value map of strings into a struct by applying `FromStr` to each field. Because of the flat structure of route params and URL queries, its significantly less flexible than something like `serde`; it also adds much less weight to your binary.
```rust
use leptos::*;
use leptos_router::*;
#[derive(Params)]
struct ContactParams {
id: usize
}
#[derive(Params)]
struct ContactSearch {
q: String
}
```
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure youre importing the right one for the derive macro.
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.
```rust
let params = use_params::<ContactParams>(cx);
let query = use_query::<ContactSearch>(cx);
// id: || -> usize
let id = move || {
params.with(|params| {
params
.map(|params| params.id)
.unwrap_or_default()
})
};
```
The untyped versions return `Memo<ParamsMap>`. Again, its memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
```rust
let params = use_params_map(cx);
let query = use_query_map(cx);
// id: || -> Option<String>
let id = move || {
params.with(|params| params.get("id").cloned())
};
```
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But its worth doing this for two reasons:
1. Its correct, i.e., it forces you to consider the cases, “What if the user doesnt pass a value for this query field? What if they pass an invalid value?”
2. Its performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replace or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we havent explained them all yet.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,23 @@
# The `<A/>` Component
Client-side navigation works perfectly fine with ordinary HTML `<a>` elements. The router adds a listener that handles every click on a `<a>` element and tries to handle it on the client side, i.e., without doing another round trip to the server to request HTML. This is what enables the snappy “single-page app” navigations youre probably familiar with from most modern web apps.
The router will bail out of handling an `<a>` click under a number of situations
- the click event has had `prevent_default()` called on it
- the <kbd>Meta</kbd>, <kbd>Alt</kbd>, <kbd>Ctrl</kbd>, or <kbd>Shift</kbd> keys were held during click
- the `<a>` has a `target` or `download` attribute, or `rel="external"`
- the link has a different origin from the current location
In other words, the router will only try to do a client-side navigation when its pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., its a link to the page youre on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if its a link to the page youre currently on, you can match this attribute with a CSS selector.
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,67 @@
# The `<Form/>` Component
Links and forms sometimes seem completely unrelated. But, in fact, they work in very similar ways.
In plain HTML, there are three ways to navigate to another page:
1. An `<a>` element that links to another page: Navigates to the URL in its `href` attribute with the `GET` HTTP method.
2. A `<form method="GET">`: Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
3. A `<form method="POST">`: Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
The router provides a [`<Form>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Form.html) component, which works like the HTML `<form>` element, but uses client-side navigations instead of full page reloads. `<Form/>` works with both `GET` and `POST` requests. With `method="GET"`, it will navigate to the URL encoded in the form data. With `method="POST"` it will make a `POST` request and handle the servers response.
`<Form/>` provides the basis for some components like `<ActionForm/>` and `<MultiActionForm/>` that well see in later chapters. But it also enables some powerful patterns of its own.
For example, imagine that you want to create a search field that updates search results in real time as the user searches, without a page reload, but that also stores the search in the URL so a user can copy and paste it to share results with someone else.
It turns out that the patterns weve learned so far make this easy to implement.
```rust
async fn fetch_results() {
// some async function to fetch our search results
}
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
// reactive access to URL query strings
let query = use_query_map(cx);
// search stored as ?q=
let search = move || query().get("q").cloned().unwrap_or_default();
// a resource driven by the search string
let search_results = create_resource(cx, search, fetch_results);
view! { cx,
<Form method="GET" action="">
<input type="search" name="search" value=search/>
<input type="submit"/>
</Form>
<Transition fallback=move || ()>
/* render search results */
</Transition>
}
}
```
Whenever you click `Submit`, the `<Form/>` will “navigate” to `?q={search}`. But because this navigation is done on the client side, theres no page flicker or reload. The URL query string changes, which triggers `search` to update. Because `search` is the source signal for the `search_results` resource, this triggers `search_results` to reload its resource. The `<Transition/>` continues displaying the current search results until the new ones have loaded. When they are complete, it switches to displaying the new result.
This is a great pattern. The data flow is extremely clear: all data flows from the URL to the resource into the UI. The current state of the application is stored in the URL, which means you can refresh the page or text the link to a friend and it will show exactly what youre expecting. And once we introduce server rendering, this pattern will prove to be really fault-tolerant, too: because it uses a `<form>` element and URLs under the hood, it actually works really well without even loading your WASM on the client.
We can actually take it a step further and do something kind of clever:
```rust
view! { cx,
<Form method="GET" action="">
<input type="search" name="search" value=search
oninput="this.form.requestSubmit()"
/>
</Form>
}
```
Youll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the users input as they type.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,23 @@
# Routing
## The Basics
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://my-cool-blog.com/blog/search?q=Search#results` consists of
- a _scheme_: `https`
- a _domain_: `my-cool-blog.com`
- a **path**: `/blog/search`
- a **query** (or **search**): `?q=Search`
- a _hash_: `#results`
The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page?
## The Philosophy
In most cases, the path should drive what is displayed on the page. From the users perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
The router handles most of this work for you by mapping the current location to particular components.

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

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

View File

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

View File

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

View File

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

View File

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

180
docs/book/src/testing.md Normal file
View File

@@ -0,0 +1,180 @@
# Testing Your Components
Testing user interfaces can be relatively tricky, but really important. This article
will discuss a couple principles and approaches for testing a Leptos app.
## 1. Test business logic with ordinary Rust tests
In many cases, it makes sense to pull the logic out of your components and test
it separately. For some simple components, theres no particular logic to test, but
for many its worth using a testable wrapping type and implementing the logic in
ordinary Rust `impl` blocks.
For example, instead of embedding logic in a component directly like this:
```rust
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
// ⚠️ this is hard to test because it's embedded in the component
let num_remaining = move || todos.with(|todos| {
todos.iter().filter(|todo| !todo.completed).sum()
});
}
```
You could pull that logic out into a separate data structure and test it:
```rust
pub struct Todos(Vec<Todo>);
impl Todos {
pub fn num_remaining(&self) -> usize {
todos.iter().filter(|todo| !todo.completed).sum()
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_remaining {
// ...
}
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
// ✅ this has a test associated with it
let num_remaining = move || todos.with(Todos::num_remaining);
}
```
In general, the less of your logic is wrapped into your components themselves, the
more idiomatic your code will feel and the easier it will be to test.
## 2. Test components with `wasm-bindgen-test`
[`wasm-bindgen-test`](https://crates.io/crates/wasm-bindgen-test) is a great utility
for integrating or end-to-end testing WebAssembly apps in a headless browser.
To use this testing utility, you need to add `wasm-bindgen-test` to your `Cargo.toml`:
```toml
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
```
You should create tests in a separate `tests` directory. You can then run your tests in the browser of your choice:
```bash
wasm-pack test --firefox
```
> To see the full setup, check out the tests for the [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) example.
### Writing Your Tests
Most tests will involve some combination of vanilla DOM manipulation and comparison to a `view`. For example, heres a test [for the
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/mod.rs).
First, we set up the testing environment.
```rust
use wasm_bindgen_test::*;
use counter::*;
use leptos::*;
use web_sys::HtmlElement;
// tell the test runner to run tests in the browser
wasm_bindgen_test_configure!(run_in_browser);
```
Im going to create a simpler wrapper for each test case, and mount it there.
This makes it easy to encapsulate the test results.
```rust
// like marking a regular test with #[test]
#[wasm_bindgen_test]
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
);
}
```
Well use some manual DOM operations to grab the `<div>` that wraps
the whole component, as well as the `clear` button.
```rust
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
```
Now we can use ordinary DOM APIs to simulate user interaction.
```rust
// now let's click the `clear` button
clear.click();
```
You can test individual DOM element attributes or text node values. Sometimes
I like to test the whole view at once. We can do this by testing the elements
`outerHTML` against our expectations.
```rust
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
);
```
That test involved us manually replicating the `view` thats inside the component.
There's actually an easier way to do this... We can just test against a `<SimpleCounter/>`
with the initial value `0`. This is where our wrapping element comes in: Ill just test
the wrappers `innerHTML` against another comparison case.
```rust
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
```
This is only a very limited introduction to testing. But I hope its useful as you begin to build applications.
> For more, see [the testing section of the `wasm-bindgen` guide](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/index.html#testing-on-wasm32-unknown-unknown-with-wasm-bindgen-test).

View File

@@ -0,0 +1,162 @@
# A Basic Component
That “Hello, world!” was a _very_ simple example. Lets move on to something a
little more like an ordinary app.
First, lets edit the `main` function so that, instead of rendering the whole
app, it just renders an `<App/>` component. Components are the basic unit of
composition and design in most web frameworks, and Leptos is no exception.
Conceptually, they are similar to HTML elements: they represent a section of the
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
`PascalCase`, so most Leptos applications will start with something like an
`<App/>` component.
```rust
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
Now lets define our `<App/>` component itself. Because its relatively simple,
Ill give you the whole thing up front, then walk through it line by line.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<button
on:click=move |_| {
set_count(3);
}
>
"Click me: "
{move || count.get()}
</button>
}
}
```
## The Component Signature
```rust
#[component]
```
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
used as a component in your Leptos application. Well see some of the other features of
this macro in a couple chapters.
```rust
fn App(cx: Scope) -> impl IntoView
```
Every component is a function with the following characteristics
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
as its first argument. This `Scope` is our entrypoint into the reactive system.
By convention, its usually named `cx`.
2. You can include other arguments, which will be available as component “props.”
3. Component functions return `impl IntoView`, which is an opaque type that includes
anything you could return from a Leptos `view`.
## The Component Body
The body of the component function is a set-up function that runs once, not a
render function that reruns multiple times. Youll typically use it to create a
few reactive variables, define any side effects that run in response to those values
changing, and describe the user interface.
```rust
let (count, set_count) = create_signal(cx, 0);
```
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
creates a signal, the basic unit of reactive change and state management in Leptos.
This returns a `(getter, setter)` tuple. To access the current value, 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.
## The View
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
```rust
view! { cx,
<button
// define an event listener with on:
on:click=move |_| {
// on stable, this is set_count.set(3);
set_count(3);
}
>
// text nodes are wrapped in quotation marks
"Click me: "
// blocks can include Rust code
{move || count.get()}
</button>
}
```
This should mostly be easy to understand: it looks like HTML, with a special
`on:click` to define a `click` event listener, a text node thats formatted like
a Rust string, and then...
```rust
{move || count.get()}
```
whatever that is.
People sometimes joke that they use more closures in their first Leptos application
than theyve ever used in their lives. And fair enough. Basically, passing a function
into the view tells the framework: “Hey, this is something that might change.”
When we click the button and call `set_count`, the `count` signal is updated. This
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
and the framework makes a targeted update to that one specific text node, touching
nothing else in your application. This is what allows for extremely efficient updates
to the DOM.
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
that this closure is redundant, at least if youre in `nightly` Rust. If youre using
Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary.
As a result, you can write a simpler view:
```rust
view! { cx,
<button /* ... */>
"Click me: "
// identical to {move || count.get()}
{count}
</button>
}
```
Remember—and this is _very important_—only functions are reactive. This means that
`{count}` and `{count()}` do very different things in your view. `{count}` passes
in a function, telling the framework to update the view every time `count` changes.
`{count()}` access the value of `count` once, and passes an `i32` into the view,
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
> and docs for whats going on. Feel free to fork the examples to play with them yourself!
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,154 @@
# `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 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
increment a counter.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
}
>
"Click me: "
{move || count()}
</button>
}
}
```
So far, this is just the example from the last chapter.
## Dynamic Classes
Now lets say Id like to update the list of CSS classes on this element dynamically.
For example, lets say I want to add the class `red` when the count is odd. I can
do this using the `class:` syntax.
```rust
class:red=move || count() % 2 == 1
```
`class:` attributes take
1. the class name, following the colon (`red`)
2. a value, which can be a `bool` or a function that returns a `bool`
When the value is `true`, the class is added. When the value is `false`, the class
is removed. And if the value is a function that accesses a signal, the class will
reactively update when the signal changes.
Now every time I click the button, the text should toggle between red and black as
the number switches between even and odd.
Some CSS class names cant be directly parsed by the `view` macro, especially if they include a mix of dashes and numbers or other characters. In that case, you can use a tuple syntax: `class=("name", value)` still directly updates a single class.
```rust
class=("button-20", move || count() % 2 == 1)
```
> If youre following along, make sure you go into your `index.html` and add something like this:
>
> ```html
> <style>
> .red {
> color: red;
> }
> </style>
> ```
## Dynamic Styles
Individual CSS properties can be directly updated with a similar `style:` syntax.
```rust
let (x, set_x) = create_signal(cx, 0);
let (y, set_y) = create_signal(cx, 0);
view! { cx,
<div
style="position: absolute"
style:left=move || format!("{}px", x() + 100)
style:top=move || format!("{}px", y() + 100)
style:background-color=move || format!("rgb({}, {}, 100)", x(), y())
style=("--columns", x)
>
"Moves when coordinates change"
</div>
}
```
## Dynamic Attributes
The same applies to plain attributes. Passing a plain string or primitive value to
an attribute gives it a static value. Passing a function (including a signal) to
an attribute causes it to update its value reactively. Lets add another element
to our view:
```rust
<progress
max="50"
// signals are functions, so this <=> `move || count.get()`
value=count
/>
```
Now every time we set the count, not only will the `class` of the `<button>` be
toggled, but the `value` of the `<progress>` bar will increase, which means that
our progress bar will move forward.
## Derived Signals
Lets go one layer deeper, just for fun.
You already know that we create reactive interfaces just by passing functions into
the `view`. This means that we can easily change our progress bar. For example,
suppose we want it to move twice as fast:
```rust
<progress
max="50"
value=move || count() * 2
/>
```
But imagine we want to reuse that calculation in more than one place. You can do this
using a **derived signal**: a closure that accesses a signal.
```rust
let double_count = move || count() * 2;
/* insert the rest of the view */
<progress
max="50"
// we use it once here
value=double_count
/>
<p>
"Double Count: "
// and again here
{double_count}
</p>
```
Derived signals let you create reactive computed values that can be used in multiple
places in your application with minimal overhead.
> Note: Using a derived signal like this means that the calculation runs once per
> signal change per place we access `double_count`; in other words, twice. This is a
> very cheap calculation, so thats fine. Well look at memos in a later chapter, which
> are designed to solve this problem for expensive calculations.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,311 @@
# Components and Props
So far, weve been building our whole application in a single component. This
is fine for really tiny examples, but in any real application youll need to
break the user interface out into multiple components, so you can break your
interface down into smaller, reusable, composable chunks.
Lets take our progress bar example. Imagine that you want two progress bars
instead of one: one that advances one tick per click, one that advances two ticks
per click.
You _could_ do this by just creating two `<progress>` elements:
```rust
let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
view! { cx,
<progress
max="50"
value=count
/>
<progress
max="50"
value=double_count
/>
}
```
But of course, this doesnt scale very well. If you want to add a third progress
bar, you need to add this code another time. And if you want to edit anything
about it, you need to edit it in triplicate.
Instead, lets create a `<ProgressBar/>` component.
```rust
#[component]
fn ProgressBar(
cx: Scope
) -> impl IntoView {
view! { cx,
<progress
max="50"
// hmm... where will we get this from?
value=progress
/>
}
}
```
Theres just one problem: `progress` is not defined. Where should it come from?
When we were defining everything manually, we just used the local variable names.
Now we need some way to pass an argument into the component.
## Component Props
We do this using component properties, or “props.” If youve used another frontend
framework, this is probably a familiar idea. Basically, properties are to components
as attributes are to HTML elements: they let you pass additional information into
the component.
In Leptos, you define props by giving additional arguments to the component function.
```rust
#[component]
fn ProgressBar(
cx: Scope,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
<progress
max="50"
// now this works
value=progress
/>
}
}
```
Now we can use our component in the main `<App/>` components view.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
// now we use our component!
<ProgressBar progress=count/>
}
}
```
Using a component in the view looks a lot like using an HTML element. Youll
notice that you can easily tell the difference between an element and a component
because components always have `PascalCase` names. You pass the `progress` prop
in as if it were an HTML element attribute. Simple.
### Reactive and Static Props
Youll notice that throughout this example, `progress` takes a reactive
`ReadSignal<i32>`, and not a plain `i32`. This is **very important**.
Component props have no special meaning attached to them. A component is simply
a function that runs once to set up the user interface. The only way to tell the
interface to respond to changing is to pass it a signal type. So if you have a
component property that will change over time, like our `progress`, it should
be a signal.
### `optional` Props
Right now the `max` setting is hard-coded. Lets take that as a prop too. But
lets add a catch: lets make this prop optional by annotating the particular
argument to the component function with `#[prop(optional)]`.
```rust
#[component]
fn ProgressBar(
cx: Scope,
// mark this prop optional
// you can specify it or not when you use <ProgressBar/>
#[prop(optional)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
<progress
max=max
value=progress
/>
}
}
```
Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max`
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
on an `optional` is its `Default::default()` value, which for a `u16` is going to
be `0`. In the case of a progress bar, a max value of `0` is not very useful.
So lets give it a particular default value instead.
### `default` props
You can specify a default value other than `Default::default()` pretty simply
with `#[prop(default = ...)`.
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(default = 100)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
<progress
max=max
value=progress
/>
}
}
```
### Generic Props
This is great. But we began with two counters, one driven by `count`, and one by
the derived signal `double_count`. Lets recreate that by using `double_count`
as the `progress` prop on another `<ProgressBar/>`.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
view! { cx,
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
<ProgressBar progress=count/>
// add a second progress bar
<ProgressBar progress=double_count/>
}
}
```
Hm... this wont compile. It should be pretty easy to understand why: weve declared
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
its a closure that returns an `i32`.
There are a couple ways to handle this. One would be to say: “Well, I know that
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
could just take any function?” If youre savvy, you may know that both these
implement the trait `Fn() -> i32`. So you could use a generic component:
```rust
#[component]
fn ProgressBar<F>(
cx: Scope,
#[prop(default = 100)]
max: u16,
progress: F
) -> impl IntoView
where
F: Fn() -> i32 + 'static,
{
view! { cx,
<progress
max=max
value=progress
/>
}
}
```
This is a perfectly reasonable way to write this component: `progress` now takes
any value that implements this `Fn()` trait.
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
> or as `progress: impl Fn() -> i32 + 'static,`, in part because theyre actually used to generate
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
### `into` Props
Theres one more way we could implement this, and it would be to use `#[prop(into)]`.
This attribute automatically calls `.into()` on the values you pass as props,
which allows you to easily pass props with different values.
In this case, its helpful to know about the
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
is an enumerated type that represents any kind of readable reactive signal. It can
be useful when defining APIs for components youll want to reuse while passing
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
reactive value.
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(default = 100)]
max: u16,
#[prop(into)]
progress: Signal<i32>
) -> impl IntoView
{
view! { cx,
<progress
max=max
value=progress
/>
}
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
view! { cx,
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
// .into() converts `ReadSignal` to `Signal`
<ProgressBar progress=count/>
// use `Signal::derive()` to wrap a derived signal
<ProgressBar progress=Signal::derive(cx, double_count)/>
}
}
```
## Documenting Components
This is one of the least essential but most important sections of this book.
Its not strictly necessary to document your components and their props. It may
be very important, depending on the size of your team and your app. But its very
easy, and bears immediate fruit.
To document a component and its props, you can simply add doc comments on the
component function, and each one of the props:
```rust
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
cx: Scope,
/// The maximum value of the progress bar.
#[prop(default = 100)]
max: u16,
/// How much progress should be displayed.
#[prop(into)]
progress: Signal<i32>,
) -> impl IntoView {
/* ... */
}
```
Thats all you need to do. These behave like ordinary Rust doc comments, except
that you can document individual component props, which cant be done with Rust
function arguments.
This will automatically generate documentation for your component, its `Props`
type, and each of the fields used to add props. It can be a little hard to
understand how powerful this is until you hover over the component name or props
and see the power of the `#[component]` macro combined with rust-analyzer here.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,108 @@
# Iteration
Whether youre listing todos, displaying a table, or showing product images,
iterating over a list of items is a common task in web applications. Reconciling
the differences between changing sets of items can also be one of the trickiest
tasks for a framework to handle well.
Leptos supports to two different patterns for iterating over items:
1. For static views: `Vec<_>`
2. For dynamic lists: `<For/>`
## Static Views with `Vec<_>`
Sometimes you need to show an item repeatedly, but the list youre drawing from
does not often change. In this case, its important to know that you can insert
any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render
`T`, you can render `Vec<T>`.
```rust
let values = vec![0, 1, 2];
view! { cx,
// this will just render "012"
<p>{values.clone()}</p>
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { cx, <li>{n}</li>})
.collect::<Vec<_>>()}
</ul>
}
```
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
```rust
let values = vec![0, 1, 2];
view! { cx,
// this will just render "012"
<p>{values.clone()}</p>
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { cx, <li>{n}</li>})
.collect_view(cx)}
</ul>
}
```
The fact that the _list_ is static doesnt mean the interface needs to be static.
You can render dynamic items as part of a static list.
```rust
// create a list of N signals
let counters = (1..=length).map(|idx| create_signal(cx, idx));
// each item manages a reactive view
// but the list itself will never change
let counter_buttons = counters
.map(|(count, set_count)| {
view! { cx,
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
>
{count}
</button>
</li>
}
})
.collect_view(cx);
view! { cx,
<ul>{counter_buttons}</ul>
}
```
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
it changes, this will rerender every item in the list. This is quite inefficient!
Fortunately, theres a better way.
## Dynamic Rendering with the `<For/>` Component
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
keyed dynamic list. It takes three props:
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
- `view`: renders each `T` into a view
`key` is, well, the key. You can add, remove, and move items within the list. As
long as each items key is stable over time, the framework does not need to rerender
any of the items, unless they are new additions, and it can very efficiently add,
remove, and move items as they change. This allows for extremely efficient updates
to the list as it changes, with minimal additional work.
Creating a good `key` can be a little tricky. You generally do _not_ want to use
an index for this purpose, as it is not stable—if you remove or move items, their
indices change.
But its a great idea to do something like generating a unique ID for each row as
it is generated, and using that as an ID for the key function.
Check out the `<DynamicList/>` component below for an example.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,114 @@
# Forms and Inputs
Forms and form inputs are an important part of interactive apps. There are two
basic patterns for interacting with inputs in Leptos, which you may recognize
if youre familiar with React, SolidJS, or a similar framework: using **controlled**
or **uncontrolled** inputs.
## Controlled Inputs
In a "controlled input," the framework controls the state of the input
element. On every `input` event, it updates a local signal that holds the current
state, which in turn updates the `value` prop of the input.
There are two important things to remember:
1. The `input` event fires on (almost) every change to the element, while the
`change` event fires (more or less) when you unfocus the input. You probably
want `on:input`, but we give you the freedom to choose.
2. The `value` _attribute_ only sets the initial value of the input, i.e., it
only updates the input up to the point that you begin typing. The `value`
_property_ continues updating the input after that. You usually want to set
`prop:value` for this reason.
```rust
let (name, set_name) = create_signal(cx, "Controlled".to_string());
view! { cx,
<input type="text"
on:input=move |ev| {
// event_target_value is a Leptos helper function
// it functions the same way as event.target.value
// in JavaScript, but smooths out some of the typecasting
// necessary to make this work in Rust
set_name(event_target_value(&ev));
}
// the `prop:` syntax lets you update a DOM property,
// rather than an attribute.
prop:value=name
/>
<p>"Name is: " {name}</p>
}
```
## Uncontrolled Inputs
In an "uncontrolled input," the browser controls the state of the input element.
Rather than continuously updating a signal to hold its value, we use a
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
the input once when we want to get its value.
In this example, we only notify the framework when the `<form>` fires a `submit`
event.
```rust
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
let input_element: NodeRef<Input> = create_node_ref(cx);
```
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
underlying DOM node. Its value will be set when the element is rendered.
```rust
let on_submit = move |ev: SubmitEvent| {
// stop the page from reloading!
ev.prevent_default();
// here, we'll extract the value from the input
let value = input_element()
// event handlers can only fire after the view
// is mounted to the DOM, so the `NodeRef` will be `Some`
.expect("<input> to exist")
// `NodeRef` implements `Deref` for the DOM element type
// this means we can call`HtmlInputElement::value()`
// to get the current value of the input
.value();
set_name(value);
};
```
Our `on_submit` handler will access the inputs value and use it to call `set_name`.
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
know it will already have been filled when we rendered the view, so its safe to
unwrap here.
We can then call `.value()` to get the value out of the input, because `NodeRef`
gives us access to a correctly-typed HTML element.
```rust
view! { cx,
<form on:submit=on_submit>
<input type="text"
value=name
node_ref=input_element
/>
<input type="submit" value="Submit"/>
</form>
<p>"Name is: " {name}</p>
}
```
The view should be pretty self-explanatory by now. Note two things:
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
This is because were just setting the initial value of the input, and letting
the browser control its state. (We could use `prop:value` instead.)
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
They are the same thing, but `node_ref` has better rust-analyzer support.)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,287 @@
# Control Flow
In most applications, you sometimes need to make a decision: Should I render this
part of the view, or not? Should I render `<ButtonA/>` or `<WidgetB/>`? This is
**control flow**.
## A Few Tips
When thinking about how to do this with Leptos, its important to remember a few
things:
1. Rust is an expression-oriented language: control-flow expressions like
`if x() { y } else { z }` and `match x() { ... }` return their values. This
makes them very useful for declarative user interfaces.
2. For any `T` that implements `IntoView`—in other words, for any type that Leptos
knows how to render—`Option<T>` and `Result<T, impl Error>` _also_ implement
`IntoView`. And just as `Fn() -> T` renders a reactive `T`, `Fn() -> Option<T>`
and `Fn() -> Result<T, impl Error>` are reactive.
3. Rust has lots of handy helpers like [Option::map](https://doc.rust-lang.org/std/option/enum.Option.html#method.map),
[Option::and_then](https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then),
[Option::ok_or](https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or),
[Result::map](https://doc.rust-lang.org/std/result/enum.Result.html#method.map),
[Result::ok](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok), and
[bool::then](https://doc.rust-lang.org/std/primitive.bool.html#method.then) that
allow you to convert, in a declarative way, between a few different standard types,
all of which can be rendered. Spending time in the `Option` and `Result` docs in particular
is one of the best ways to level up your Rust game.
4. And always remember: to be reactive, values must be functions. Youll see me constantly
wrap things in a `move ||` closure, below. This is to ensure that they actually rerun
when the signal they depend on changes, keeping the UI reactive.
## So What?
To connect the dots a little: this means that you can actually implement most of
your control flow with native Rust code, without any control-flow components or
special knowledge.
For example, lets start with a simple signal and derived signal:
```rust
let (value, set_value) = create_signal(cx, 0);
let is_odd = move || value() & 1 == 1;
```
> If you dont recognize whats going on with `is_odd`, dont worry about it
> too much. Its just a simple way to test whether an integer is odd by doing a
> bitwise `AND` with `1`.
We can use these signals and ordinary Rust to build most control flow.
### `if` statements
Lets say I want to render some text if the number is odd, and some other text
if its even. Well, how about this?
```rust
view! { cx,
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
}
```
An `if` expression returns its value, and a `&str` implements `IntoView`, so a
`Fn() -> &str` implements `IntoView`, so this... just works!
### `Option<T>`
Lets say we want to render some text if its odd, and nothing if its even.
```rust
let message = move || {
if is_odd() {
Some("Ding ding ding!")
} else {
None
}
};
view! { cx,
<p>{message}</p>
}
```
This works fine. We can make it a little shorter if wed like, using `bool::then()`.
```rust
let message = move || is_odd().then(|| "Ding ding ding!");
view! { cx,
<p>{message}</p>
}
```
You could even inline this if youd like, although personally I sometimes like the
better `cargo fmt` and `rust-analyzer` support I get by pulling things out of the `view`.
### `match` statements
Were still just writing ordinary Rust code, right? So you have all the power of Rusts
pattern matching at your disposal.
```rust
let message = move || {
match value() {
0 => "Zero",
1 => "One",
n if is_odd() => "Odd",
_ => "Even"
}
};
view! { cx,
<p>{message}</p>
}
```
And why not? YOLO, right?
## Preventing Over-Rendering
Not so YOLO.
Everything weve just done is basically fine. But theres one thing you should remember
and try to be careful with. Each one of the control-flow functions weve created so far
is basically a derived signal: it will rerun every time the value changes. In the examples
above, where the value switches from even to odd on every change, this is fine.
But consider the following example:
```rust
let (value, set_value) = create_signal(cx, 0);
let message = move || if value() > 5 {
"Big"
} else {
"Small"
};
view! { cx,
<p>{message}</p>
}
```
This _works_, for sure. But if you added a log, you might be surprised
```rust
let message = move || if value() > 5 {
log!("{}: rendering Big", value());
"Big"
} else {
log!("{}: rendering Small", value());
"Small"
};
```
As a user clicks a button, youd see something like this:
```
1: rendering Small
2: rendering Small
3: rendering Small
4: rendering Small
5: rendering Small
6: rendering Big
7: rendering Big
8: rendering Big
... ad infinitum
```
Every time `value` changes, it reruns the `if` statement. This makes sense, with
how reactivity works. But it has a downside. For a simple text node, rerunning
the `if` statement and rerendering isnt a big deal. But imagine it were
like this:
```rust
let message = move || if value() > 5 {
<Big/>
} else {
<Small/>
};
```
This rerenders `<Small/>` five times, then `<Big/>` infinitely. If theyre
loading resources, creating signals, or even just creating DOM nodes, this is
unnecessary work.
### `<Show/>`
The [`<Show/>`](https://docs.rs/leptos/latest/leptos/fn.Show.html) component is
the answer. You pass it a `when` condition function, a `fallback` to be shown if
the `when` function returns `false`, and children to be rendered if `when` is `true`.
```rust
let (value, set_value) = create_signal(cx, 0);
view! { cx,
<Show
when=move || { value() > 5 }
fallback=|cx| view! { cx, <Small/> }
>
<Big/>
</Show>
}
```
`<Show/>` memoizes the `when` condition, so it only renders its `<Small/>` once,
continuing to show the same component until `value` is greater than five;
then it renders `<Big/>` once, continuing to show it indefinitely.
This is a helpful tool to avoid rerendering when using dynamic `if` expressions.
As always, there's some overhead: for a very simple node (like updating a single
text node, or updating a class or attribute), a `move || if ...` will be more
efficient. But if its at all expensive to render either branch, reach for
`<Show/>`.
## Note: Type Conversions
Theres one final thing its important to say in this section.
The `view` macro doesnt return the most-generic wrapping type
[`View`](https://docs.rs/leptos/latest/leptos/enum.View.html).
Instead, it returns things with types like `Fragment` or `HtmlElement<Input>`. This
can be a little annoying if youre returning different HTML elements from
different branches of a conditional:
```rust,compile_error
view! { cx,
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }
}}
</main>
}
```
This strong typing is actually very powerful, because
[`HtmlElement`](https://docs.rs/leptos/0.1.3/leptos/struct.HtmlElement.html) is,
among other things, a smart pointer: each `HtmlElement<T>` type implements
`Deref` for the appropriate underlying `web_sys` type. In other words, in the browser
your `view` returns real DOM elements, and you can access native DOM methods on
them.
But it can be a little annoying in conditional logic like this, because you cant
return different types from different branches of a condition in Rust. There are two ways
to get yourself out of this situation:
1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>`
with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any)
2. If you have a variety of view types that are not all `HtmlElement`, convert them to
`View`s with [`.into_view(cx)`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
Heres the same example, with the conversion added:
```rust,compile_error
view! { cx,
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }.into_any()
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
}}
</main>
}
```
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,115 @@
# Error Handling
[In the last chapter](./06_control_flow.md), we saw that you can render `Option<T>`:
in the `None` case, it will render nothing, and in the `T` case, it will render `T`
(that is, if `T` implements `IntoView`). You can actually do something very similar
with a `Result<T, E>`. In the `Err(_)` case, it will render nothing. In the `Ok(T)`
case, it will render the `T`.
Lets start with a simple component to capture a number input.
```rust
#[component]
fn NumericInput(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
// when input changes, try to parse a number from the input
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
<label>
"Type a number (or not!)"
<input on:input=on_input/>
<p>
"You entered "
<strong>{value}</strong>
</p>
</label>
}
}
```
Every time you change the input, `on_input` will attempt to parse its value into a 32-bit
integer (`i32`), and store it in our `value` signal, which is a `Result<i32, _>`. If you
type the number `42`, the UI will display
```
You entered 42
```
But if you type the string`foo`, it will display
```
You entered
```
This is not great. It saves us using `.unwrap_or_default()` or something, but it would be
much nicer if we could catch the error and do something with it.
You can do that, with the [`<ErrorBoundary/>`](https://docs.rs/leptos/latest/leptos/fn.ErrorBoundary.html)
component.
## `<ErrorBoundary/>`
An `<ErrorBoundary/>` is a little like the `<Show/>` component we saw in the last chapter.
If everythings okay—which is to say, if everything is `Ok(_)`—it renders its children.
But if theres an `Err(_)` rendered among those children, it will trigger the
`<ErrorBoundary/>`s `fallback`.
Lets add an `<ErrorBoundary/>` to this example.
```rust
#[component]
fn NumericInput(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input on:input=on_input/>
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors as strings, if we'd like
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect_view(cx)
}
</ul>
</div>
}
>
<p>"You entered " <strong>{value}</strong></p>
</ErrorBoundary>
</label>
}
}
```
Now, if you type `42`, `value` is `Ok(42)` and youll see
```
You entered 42
```
If you type `foo`, value is `Err(_)` and the `fallback` will render. Weve chosen to render
the list of errors as a `String`, so youll see something like
```
Not a number! Errors:
- cannot parse integer from empty string
```
If you fix the error, the error message will disappear and the content youre wrapping in
an `<ErrorBoundary/>` will appear again.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,290 @@
# Parent-Child Communication
You can think of your application as a nested tree of components. Each component
handles its own local state and manages a section of the user interface, so
components tend to be relatively self-contained.
Sometimes, though, youll want to communicate between a parent component and its
child. For example, imagine youve defined a `<FancyButton/>` component that adds
some styling, logging, or something else to a `<button/>`. You want to use a
`<FancyButton/>` in your `<App/>` component. But how can you communicate between
the two?
Its easy to communicate state from a parent component to a child component. We
covered some of this in the material on [components and props](./03_components.md).
Basically if you want the parent to communicate to the child, you can pass a
[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) as a prop.
But what about the other direction? How can a child send notifications about events
or state changes back up to the parent?
There are four basic patterns of parent-child communication in Leptos.
## 1. Pass a [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html)
One approach is simply to pass a `WriteSignal` from the parent down to the child, and update
it in the child. This lets you manipulate the state of the parent from the child.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<ButtonA setter=set_toggled/>
}
}
#[component]
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> impl IntoView {
view! { cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
"Toggle"
</button>
}
}
```
This pattern is simple, but you should be careful with it: passing around a `WriteSignal`
can make it hard to reason about your code. In this example, its pretty clear when you
read `<App/>` that you are handing off the ability to mutate `toggled`, but its not at
all clear when or how it will change. In this small, local example its easy to understand,
but if you find yourself passing around `WriteSignal`s like this throughout your code,
you should really consider whether this is making it too easy to write spaghetti code.
## 2. Use a Callback
Another approach would be to pass a callback to the child: say, `on_click`.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
}
}
#[component]
pub fn ButtonB<F>(
cx: Scope,
on_click: F,
) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
{
view! { cx,
<button on:click=on_click>
"Toggle"
</button>
}
}
```
Youll notice that whereas `<ButtonA/>` was given a `WriteSignal` and decided how to mutate it,
`<ButtonB/>` simply fires an event: the mutation happens back in `<App/>`. This has the advantage
of keeping local state local, preventing the problem of spaghetti mutation. But it also means
the logic to mutate that signal needs to exist up in `<App/>`, not down in `<ButtonB/>`. These
are real trade-offs, not a simple right-or-wrong choice.
> Note the way we declare the generic type `F` here for the callback. If youre
> confused, look back at the [generic props](./03_components.html#generic-props) section
> of the chapter on components.
## 3. Use an Event Listener
You can actually write Option 2 in a slightly different way. If the callback maps directly onto
a native DOM event, you can add an `on:` listener directly to the place you use the component
in your `view` macro in `<App/>`.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
// note the on:click instead of on_click
// this is the same syntax as an HTML element event listener
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
}
}
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
view! { cx,
<button>"Toggle"</button>
}
}
```
This lets you write way less code in `<ButtonC/>` than you did for `<ButtonB/>`,
and still gives a correctly-typed event to the listener. This works by adding an
`on:` event listener to each element that `<ButtonC/>` returns: in this case, just
the one `<button>`.
Of course, this only works for actual DOM events that youre passing directly through
to the elements youre rendering in the component. For more complex logic that
doesnt map directly onto an element (say you create `<ValidatedForm/>` and want an
`on_valid_form_submit` callback) you should use Option 2.
## 4. Providing a Context
This version is actually a variant on Option 1. Say you have a deeply-nested component
tree:
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout/>
}
}
#[component]
pub fn Layout(cx: Scope) -> impl IntoView {
view! { cx,
<header>
<h1>"My Page"</h1>
</header>
<main>
<Content/>
</main>
}
}
#[component]
pub fn Content(cx: Scope) -> impl IntoView {
view! { cx,
<div class="content">
<ButtonD/>
</div>
}
}
#[component]
pub fn ButtonD<F>(cx: Scope) -> impl IntoView {
todo!()
}
```
Now `<ButtonD/>` is no longer a direct child of `<App/>`, so you cant simply
pass your `WriteSignal` to its props. You could do whats sometimes called
“prop drilling,” adding a prop to each layer between the two:
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout set_toggled/>
}
}
#[component]
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
view! { cx,
<header>
<h1>"My Page"</h1>
</header>
<main>
<Content set_toggled/>
</main>
}
}
#[component]
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
view! { cx,
<div class="content">
<ButtonD set_toggled/>
</div>
}
}
#[component]
pub fn ButtonD<F>(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
todo!()
}
```
This is a mess. `<Layout/>` and `<Content/>` dont need `set_toggled`; they just
pass it through to `<ButtonD/>`. But I need to declare the prop in triplicate.
This is not only annoying but hard to maintain: imagine we add a “half-toggled”
option and the type of `set_toggled` needs to change to an `enum`. We have to change
it in three places!
Isnt there some way to skip levels?
There is!
### The Context API
You can provide data that skips levels by using [`provide_context`](https://docs.rs/leptos/latest/leptos/fn.provide_context.html)
and [`use_context`](https://docs.rs/leptos/latest/leptos/fn.use_context.html). Contexts are identified
by the type of the data you provide (in this example, `WriteSignal<bool>`), and they exist in a top-down
tree that follows the contours of your UI tree. In this example, we can use context to skip the
unnecessary prop drilling.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
// share `set_toggled` with all children of this component
provide_context(cx, set_toggled);
view! { cx,
<p>"Toggled? " {toggled}</p>
<Layout/>
}
}
// <Layout/> and <Content/> omitted
#[component]
pub fn ButtonD(cx: Scope) -> impl IntoView {
// use_context searches up the context tree, hoping to
// find a `WriteSignal<bool>`
// in this case, I .expect() because I know I provided it
let setter = use_context::<WriteSignal<bool>>(cx)
.expect("to have found the setter provided");
view! { cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
"Toggle"
</button>
}
}
```
The same caveats apply to this as to `<ButtonA/>`: passing a `WriteSignal`
around should be done with caution, as it allows you to mutate state from
arbitrary parts of your code. But when done carefully, this can be one of
the most effective techniques for global state management in Leptos: simply
provide the state at the highest level youll need it, and use it wherever
you need it lower down.
Note that there are no performance downsides to this approach. Because you
are passing a fine-grained reactive signal, _nothing happens_ in the intervening
components (`<Layout/>` and `<Content/>`) when you update it. You are communicating
directly between `<ButtonD/>` and `<App/>`. In fact—and this is the power of
fine-grained reactivity—you are communicating directly between a button click
in `<ButtonD/>` and a single text node in `<App/>`. Its as if the components
themselves dont exist at all. And, well... at runtime, they dont. Its just
signals and effects, all the way down.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,128 @@
# Component Children
Its pretty common to want to pass children into a component, just as you can pass
children into an HTML element. For example, imagine I have a `<FancyForm/>` component
that enhances an HTML `<form>`. I need some way to pass all its inputs.
```rust
view! { cx,
<Form>
<fieldset>
<label>
"Some Input"
<input type="text" name="something"/>
</label>
</fieldset>
<button>"Submit"</button>
</Form>
}
```
How can you do this in Leptos? There are basically two ways to pass components to
other components:
1. **render props**: properties that are functions that return a view
2. the **`children`** prop: a special component property that includes anything
you pass as a child to the component.
In fact, youve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
```rust
view! { cx,
<Show
// `when` is a normal prop
when=move || value() > 5
// `fallback` is a "render prop": a function that returns a view
fallback=|cx| view! { cx, <Small/> }
>
// `<Big/>` (and anything else here)
// will be given to the `children` prop
<Big/>
</Show>
}
```
Lets define a component that takes some children and a render prop.
```rust
#[component]
pub fn TakesChildren<F, IV>(
cx: Scope,
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,
/// `children` takes the `Children` type
children: Children,
) -> impl IntoView
where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
<h2>"Render Prop"</h2>
{render_prop()}
<h2>"Children"</h2>
{children(cx)}
}
}
```
`render_prop` and `children` are both functions, so we can call them to generate
the appropriate views. `children`, in particular, is an alias for
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
We can use the component like this:
```rust
view! { cx,
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
// these get passed to `children`
"Some text"
<span>"A span"</span>
</TakesChildren>
}
```
## Manipulating Children
The [`Fragment`](https://docs.rs/leptos/latest/leptos/struct.Fragment.html) type is
basically a way of wrapping a `Vec<View>`. You can insert it anywhere into your view.
But you can also access those inner views directly to manipulate them. For example, heres
a component that takes its children and turns them into an unordered list.
```rust
#[component]
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
// Fragment has `nodes` field that contains a Vec<View>
let children = children(cx)
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect_view(cx);
view! { cx,
<ul>{children}</ul>
}
}
```
Calling it like this will create a list:
```rust
view! { cx,
<WrappedChildren>
"A"
"B"
"C"
</WrappedChildren>
}
```
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -0,0 +1,5 @@
# Building User Interfaces
This first section will introduce you to the basic tools you need to build a reactive
user interface using Leptos. By the end of this section, you should be able to
build a simple, synchronous application that is rendered in the browser.

BIN
docs/video/async.mov Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

47
examples/Makefile.toml Normal file
View File

@@ -0,0 +1,47 @@
extend = [{ path = "./cargo-make/main.toml" }]
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
"counters",
"counters_stable",
"counter_without_macros",
"error_boundary",
"errors_axum",
"fetch",
"hackernews",
"hackernews_axum",
"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.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,6 @@
[tasks.integration-test]
dependencies = ["cargo-leptos-e2e"]
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]

View File

@@ -0,0 +1,28 @@
[tasks.clean]
dependencies = [
"clean-cargo",
"clean-trunk",
"clean-node_modules",
"clean-playwright",
]
[tasks.clean-cargo]
command = "cargo"
args = ["clean"]
[tasks.clean-trunk]
command = "trunk"
args = ["clean"]
[tasks.clean-node_modules]
script = '''
project_dir=${PWD##*/}
if [ "$project_dir" != "todomvc" ]; then
find . -type d -name node_modules | xargs rm -rf
fi
'''
[tasks.clean-playwright]
script = '''
find . -name playwright-report -name playwright -name test-results | xargs rm -rf
'''

View File

@@ -0,0 +1,9 @@
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[tasks.check-style]
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]

View File

@@ -0,0 +1,32 @@
extend = [
{ path = "../cargo-make/clean.toml" },
{ path = "../cargo-make/lint.toml" },
{ path = "../cargo-make/node.toml" },
]
# CI Stages
[tasks.ci]
dependencies = ["prepare", "lint", "build", "test-flow", "integration-test"]
[tasks.ci-clean]
dependencies = ["ci", "clean"]
[tasks.prepare]
dependencies = ["setup-node"]
[tasks.lint]
dependencies = ["check-style"]
[tasks.integration-test]
# ALIASES
[tasks.verify-flow]
alias = "ci"
[tasks.t]
dependencies = ["test-flow"]
[tasks.it]
alias = "integration-test"

View File

@@ -0,0 +1,43 @@
[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,4 @@
extend = [{ path = "../cargo-make/playwright.toml" }]
[tasks.integration-test]
dependencies = ["test-playwright-autostart"]

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