Compare commits

...

208 Commits

Author SHA1 Message Date
Greg Johnston
fefc1db02f build(examples): update Makefiles for recent examples 2023-07-24 11:55:33 -04:00
Greg Johnston
274b105676 docs: fix messed up component closing tags router docs 2023-07-24 11:24:58 -04:00
Greg Johnston
a689d1b4c0 docs: note on optional generic component props 2023-07-24 07:52:40 -04:00
Greg Johnston
1581e91317 docs: note on how to opt out of client-side routing 2023-07-24 07:52:29 -04:00
Andrew Grande
20f4034c1c docs: proofreading and fixing the links (#1425)
* Update 23_ssr_modes.md

Fixed grammar, added the section anchor links

* Fixed a broken link

The github link doesn't get properly rendered in the Leptos book site. Make the book work, 'break' the github link.

* Update 26_extractors.md

Fixed broken Axum links. Added an Axum extract function doc link for consistency (had Actix, but not Axum before)
2023-07-24 07:25:02 -04:00
Jason Hansen
9fb1c4b67c docs/warnings: fix warning message about updating a signal after it has been disposed (#1423)
* Remove "either" because it didn't make sense in the sentence
* Change "cause cause" to "not cause"
2023-07-24 07:24:06 -04:00
Ari Seyhun
2e559d6a06 feat: add create_query_signal for URL-synced signals (#1377) 2023-07-23 12:20:15 -04:00
Sebastian Dobe
71de6c395b feat: create a <AnimatedShow> component (closes #1370) (#1386) 2023-07-23 07:46:47 -04:00
Vladimir Motylenko
b09f9e4814 feat: Update rstml to v0.11.0 (#1416) 2023-07-23 07:46:33 -04:00
Greg Johnston
ec4bfb0e8a chore: resolve clippy incorrect_clone_impl_on_copy_type (closes #1401) (#1418) 2023-07-22 22:14:52 -04:00
Greg Johnston
39bf38d1e4 docs: CONTRIBUTING.md with helpful info re: CI (#1415) 2023-07-22 08:26:46 -04:00
Joshua Marsh
e6fd1379b8 docs: fix typo for WrapsChildren (#1402) 2023-07-22 08:26:36 -04:00
Doug A
1d9931a5a8 docs: typo in 01_basic_component.md (#1412)
typo fix
2023-07-22 08:24:30 -04:00
Greg Johnston
06164d34b5 docs: note about typed params on stable (#1414) 2023-07-22 08:23:05 -04:00
Joseph Cruz
f3de288e19 build(examples): make it easy to see which examples do what kind of testing (#1411)
* build(examples): list example projects with CI tests runners

* build(examples): add show all mode
2023-07-22 08:13:49 -04:00
Greg Johnston
62bf315059 fix: <use_/> as typed top-level element in view (#1410) 2023-07-21 10:07:34 -04:00
Greg Johnston
011c97e3a4 fix: closing element names wrong for svg::, math::, and use_ (closes #1403) (#1406) 2023-07-20 17:03:30 -04:00
Greg Johnston
2ca3d2c7a4 fix: RawText/unquoted text nodes in SSR (closes #1384) (#1407) 2023-07-20 17:03:19 -04:00
Greg Johnston
cc52c94348 docs/examples: use shorthand form for <Route/> views when possible (#1375) 2023-07-20 16:28:43 -04:00
Andrew Grande
4b8cc96dfa docs: typo in 16_routes.md
Fixed grammar
2023-07-20 16:13:25 -04:00
Greg Johnston
338d2ab839 Merge pull request #1379 from agilarity/lint-with-clippy
ci: lint with clippy
2023-07-20 14:15:16 -04:00
Greg Johnston
54fc6da24e feat: implement Resource::dispose() (#1393) 2023-07-20 14:14:49 -04:00
Andrew Grande
825b3fb858 chore: typo in README.md (#1399) 2023-07-20 14:00:43 -04:00
Andrew Grande
fd0212a142 docs: typo in 15_global_state.md (#1395)
Proofreading
2023-07-20 08:57:12 -04:00
Greg Johnston
3b397cb39c examples: remove random <form> (#1398) 2023-07-20 08:56:49 -04:00
martin frances
1e002c2c2f chore: Removed call to .into(), plus minor touch to docs. (#1396) 2023-07-20 08:07:31 -04:00
Greg Johnston
8f45daeca8 docs: correct docs for create_memo to reflect laziness (#1388) 2023-07-19 14:50:34 -04:00
Joseph Cruz
105ef989b7 ci: install clippy 2023-07-19 08:54:05 -04:00
Greg Johnston
9e7c31d1e4 docs: small issues (#1385) 2023-07-19 08:53:37 -04:00
Joseph Cruz
771dfa6b68 ci: lint with clippy 2023-07-19 08:44:36 -04:00
Joseph Cruz
fb52cfa73e fix: needless_raw_string_hashes 2023-07-19 08:43:57 -04:00
Ari Seyhun
b2c75d215b chore: remove unnecessary string allocation in TryFrom for Url (#1376) 2023-07-19 07:04:06 -04:00
Andrew Grande
951607de74 docs: typos
* Fixed wording

* Update ARCHITECTURE.md

Fixed superfluous whitespace
2023-07-19 07:03:50 -04:00
Joseph Cruz
122fd2bc74 fix: useless_conversion 2023-07-18 20:56:55 -04:00
Joseph Cruz
f102125d3c fix: needless_borrow 2023-07-18 20:56:55 -04:00
Joseph Cruz
14bda76b30 fix: needless_raw_string_hashes (allow) 2023-07-18 20:56:39 -04:00
Joseph Cruz
3af115a663 fix: maybe_misused_cfg 2023-07-18 20:56:39 -04:00
Joseph Cruz
a344804734 fix: incorrect_clone_impl_on_copy_type (allow) 2023-07-18 20:54:51 -04:00
Greg Johnston
c1c49ce53b v0.4.5 2023-07-18 14:02:56 -04:00
Joseph Cruz
d8eaa5c004 build: use cargo hack for clippy 2023-07-18 08:02:24 -04:00
Greg Johnston
e8aa9b24f1 fix: memory leak in leptos_axum (#1374) 2023-07-17 21:59:20 -04:00
Greg Johnston
3036cd223e v0.4.4 2023-07-17 17:33:44 -04:00
Greg Johnston
1ae5150b08 fix: release lock on stored values during update/set (#1373) 2023-07-17 14:19:03 -04:00
Greg Johnston
47148f2033 perf: exclude hydration code in CSR mode (#1372) 2023-07-17 12:20:33 -04:00
Sebastian Probst Eide
4d4d15436b fix: incorrect tree walker filter in hot reloading (closes #1355) (#1368) 2023-07-17 08:44:32 -04:00
Jack DeVries
7f4741b3a3 doc: add previews to backup CodeSandbox (#1169) 2023-07-17 08:40:30 -04:00
Hans Baker
85644a7c1c Fix broken link to example code in testing book page (#1365) 2023-07-16 20:00:56 -04:00
Greg Johnston
f40ae6af30 fix: correctly show fallback for Transition on first load even if not hydrating (#1362) 2023-07-16 15:19:51 -04:00
Greg Johnston
708e1a5aab docs: wasm-pack instructions missing --debug for Tailwind examples 2023-07-16 12:28:20 -04:00
Joseph Cruz
55613c9a31 ci(check-examples): only run on source change (#1356)
* ci(check-examples): only run on source change

* ci(check-examples):  simulate source change

* ci(check-examples): fix expression

* ci(check-examples): simulate source change

* ci(check-examples): test change against files

* ci(check-examples): adjust expression

* ci(check-examples): remove quotes

* ci(check-examples): use from json

* ci(check-examples): set output value

* ci(check-examples): remove simulated change
2023-07-15 19:09:54 -04:00
Joseph Cruz
e6590c7d31 ci(ci): only run on source change (#1357)
* ci(ci): only run on source change

* ci(ci): simulate source change

* ci(ci): remove simulated source change
2023-07-15 19:09:33 -04:00
Greg Johnston
5af2f4e98d docs/warning: fix <ActionForm/> docs and add runtime warning for incorrect encodings (#1360) 2023-07-15 19:09:03 -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
373 changed files with 13815 additions and 3919 deletions

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

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

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

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

View File

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

View File

@@ -1,45 +1,74 @@
name: Test
name: Check Examples
on:
push:
branches: [main]
branches:
- main
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
branches:
- main
jobs:
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
setup:
name: Get Examples
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
- 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"
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make check-examples
matrix-job:
name: Check
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
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,6 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:

View File

@@ -1,45 +0,0 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Run `cargo check` ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- 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: Run cargo check on all libraries
run: cargo make --profile=github-actions check

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

@@ -0,0 +1,75 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
setup:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
matrix-job:
name: CI
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
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"

View File

@@ -1,34 +0,0 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Run rustfmt
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Run Rustfmt
run: cargo fmt -- --check

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

@@ -1,45 +0,0 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- 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: Run tests with all features
run: cargo make --profile=github-actions test

View File

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

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

94
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,94 @@
# 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`.
## Before Submitting a PR
We have a fairly extensive CI setup that runs both lints (like `rustfmt` and `clippy`)
and tests on PRs. You can run most of these locally if you have `cargo-make` installed.
If you added an example, make sure to add it to the list in `examples/Makefile.toml`.
From the root directory of the repo, run
- `cargo +nightly fmt`
- `cargo +nightly make check`
- `cargo +nightly make test`
- `cargo +nightly make check-examples`
- `cargo +nightly make --profile=github-actions ci`
If you modified an example:
- `cd examples/your_example`
- `cargo +nightly fmt -- --config-path ../..`
- `cargo +nightly make --profile=github-actions verify-flow`
## Architecture
See [ARCHITECTURE.md](./ARCHITECTURE.md).

View File

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

View File

@@ -3,118 +3,25 @@
# cargo install --force cargo-make
############
[config]
# make tasks run at the workspace root
default_to_workspace = false
[tasks.check]
clear = true
dependencies = [
"check-all",
"check-wasm",
"check-all-release",
"check-wasm-release",
]
[tasks.check-all]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-wasm]
clear = true
dependencies = [{ name = "check-wasm", path = "leptos" }]
[tasks.check-all-release]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-wasm-release]
clear = true
dependencies = [{ name = "check-wasm-release", path = "leptos" }]
[tasks.check-examples]
clear = true
dependencies = [
{ name = "check", path = "examples/counter" },
{ name = "check", path = "examples/counter_isomorphic" },
{ name = "check", path = "examples/counters" },
{ name = "check", path = "examples/error_boundary" },
{ name = "check", path = "examples/errors_axum" },
{ name = "check", path = "examples/fetch" },
{ name = "check", path = "examples/hackernews" },
{ name = "check", path = "examples/hackernews_axum" },
{ name = "check", path = "examples/login_with_token_csr_only" },
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
{ name = "check", path = "examples/session_auth_axum" },
{ name = "check", path = "examples/slots" },
{ name = "check", path = "examples/ssr_modes" },
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/tailwind_csr_trunk" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todo_app_sqlite_viz" },
{ name = "check", path = "examples/todomvc" },
]
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
[tasks.check-stable]
workspace = false
clear = true
dependencies = [
{ name = "check", path = "examples/counter_without_macros" },
{ name = "check", path = "examples/counters_stable" },
]
[tasks.test]
clear = true
dependencies = [
"test-all",
"test-leptos_macro-example",
"doc-leptos_macro-example",
]
[tasks.test-all]
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "test", "--doc"]
cwd = "leptos_macro/example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "doc"]
cwd = "leptos_macro/example"
install_crate = false
[tasks.test-examples]
description = "Run all unit and web tests for examples"
[tasks.ci-examples]
workspace = false
cwd = "examples"
command = "cargo"
args = ["make", "test-unit-and-web"]
[tasks.verify-examples]
description = "Run all quality checks and tests for examples"
cwd = "examples"
command = "cargo"
args = ["make", "verify-flow"]
args = ["make", "ci-clean"]
[tasks.clean-examples]
description = "Clean all example projects"
workspace = false
cwd = "examples"
command = "cargo"
args = ["make", "clean-all"]
[env]
RUSTFLAGS = ""
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"
args = ["make", "clean"]

View File

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

View File

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

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"

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

@@ -0,0 +1,11 @@
[tasks.lint]
dependencies = ["check-format-flow", "clippy-each-feature"]
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../" }
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
[tasks.clippy-each-feature]
dependencies = ["install-clippy"]
command = "cargo"
args = ["hack", "clippy", "--all", "--each-feature", "--no-dev-deps"]

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

@@ -0,0 +1,15 @@
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"]

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"

2
docs/book/book.toml Normal file
View File

@@ -0,0 +1,2 @@
[output.html.playground]
runnable = false

View File

@@ -14,24 +14,38 @@ If you dont already have it installed, you can install Trunk by running
cargo install trunk
```
Create a basic Rust binary project
Create a basic Rust project
```bash
cargo init leptos-tutorial
```
> We recommend using `nightly` Rust, as it enables [a few nice features](https://github.com/leptos-rs/leptos#nightly-note). To use `nightly` Rust with WebAssembly, you can run
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos --features=csr,nightly
```
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
> rustup target add wasm32-unknown-unknown
> ```
>
> 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.
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
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
cargo add leptos
rustup target add wasm32-unknown-unknown
```
Create a simple `index.html` in the root of the `leptos-tutorial` directory

View File

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

View File

@@ -12,7 +12,10 @@
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.md)
- [Interlude: Reactivity and Functions](./interlude_functions.md)
- [Reactivity](./reactivity/README.md)
- [Working with Signals](./reactivity/working_with_signals.md)
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
- [Interlude: Reactivity and Functions](./reactivity/interlude_functions.md)
- [Testing](./testing.md)
- [Async](./async/README.md)
- [Loading Data with Resources](./async/10_resources.md)
@@ -20,7 +23,6 @@
- [Transition](./async/12_transition.md)
- [Actions](./async/13_actions.md)
- [Interlude: Projecting Children](./interlude_projecting_children.md)
- [Responding to Changes with `create_effect`](./14_create_effect.md)
- [Global State Management](./15_global_state.md)
- [Router](./router/README.md)
- [Defining `<Routes/>`](./router/16_routes.md)
@@ -29,7 +31,7 @@
- [`<A/>`](./router/19_a.md)
- [`<Form/>`](./router/20_form.md)
- [Interlude: Styling](./interlude_styling.md)
- [Metadata]()
- [Metadata](./metadata.md)
- [Server Side Rendering](./ssr/README.md)
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
- [The Life of a Page Load](./ssr/22_life_cycle.md)
@@ -37,16 +39,9 @@
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
- [Working with the Server](./server/README.md)
- [Server Functions](./server/25_server_functions.md)
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
- [Headers]()
- [Cookies]()
- [Building Full-Stack Apps]()
- [Actions]()
- [Forms]()
- [`<ActionForm/>`s]()
- [Turning off WebAssembly: Progressive Enhancement and Graceful Degradation]()
- [Advanced Reactivity]()
- [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

@@ -47,6 +47,12 @@ Note that if you're using this with SSR too, the same Cargo profile will be appl
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.

View File

@@ -53,3 +53,89 @@ Resources also provide a `refetch()` method that allows you to manually reload t
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use gloo_timers::future::TimeoutFuture;
use leptos::*;
// Here we define an async function
// This could be anything: a network request, database read, etc.
// Here, we just multiply a number by 10
async fn load_data(value: i32) -> i32 {
// fake a one-second delay
TimeoutFuture::new(1_000).await;
value * 10
}
#[component]
fn App(cx: Scope) -> impl IntoView {
// this count is our synchronous, local state
let (count, set_count) = create_signal(cx, 0);
// create_resource takes two arguments after its scope
let async_data = create_resource(
cx,
// the first is the "source signal"
count,
// the second is the loader
// it takes the source signal's value as its argument
// and does some async work
|value| async move { load_data(value).await },
);
// whenever the source signal changes, the loader reloads
// you can also create resources that only load once
// just return the unit type () from the source signal
// that doesn't depend on anything: we just load it once
let stable = create_resource(cx, || (), |_| async move { load_data(1).await });
// we can access the resource values with .read()
// this will reactively return None before the Future has resolved
// and update to Some(T) when it has resolved
let async_result = move || {
async_data
.read(cx)
.map(|value| format!("Server returned {value:?}"))
// This loading state will only show before the first load
.unwrap_or_else(|| "Loading...".into())
};
// the resource's loading() method gives us a
// signal to indicate whether it's currently loading
let loading = async_data.loading();
let is_loading = move || if loading() { "Loading..." } else { "Idle." };
view! { cx,
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
}
>
"Click me"
</button>
<p>
<code>"stable"</code>": " {move || stable.read(cx)}
</p>
<p>
<code>"count"</code>": " {count}
</p>
<p>
<code>"async_value"</code>": "
{async_result}
<br/>
{is_loading}
</p>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -72,3 +72,58 @@ This inversion of the flow of control makes it easier to add or remove individua
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use gloo_timers::future::TimeoutFuture;
use leptos::*;
async fn important_api_call(name: String) -> String {
TimeoutFuture::new(1_000).await;
name.to_ascii_uppercase()
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (name, set_name) = create_signal(cx, "Bill".to_string());
// this will reload every time `name` changes
let async_data = create_resource(
cx,
name,
|name| async move { important_api_call(name).await },
);
view! { cx,
<input
on:input=move |ev| {
set_name(event_target_value(&ev));
}
prop:value=name
/>
<p><code>"name:"</code> {name}</p>
<Suspense
// the fallback will show whenever a resource
// read "under" the suspense is loading
fallback=move || view! { cx, <p>"Loading..."</p> }
>
// the children will be rendered once initially,
// and then whenever any resources has been resolved
<p>
"Your shouting name is "
{move || async_data.read(cx)}
</p>
</Suspense>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -9,3 +9,76 @@ This example shows how you can create a simple tabbed contact list with `<Transi
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use gloo_timers::future::TimeoutFuture;
use leptos::*;
async fn important_api_call(id: usize) -> String {
TimeoutFuture::new(1_000).await;
match id {
0 => "Alice",
1 => "Bob",
2 => "Carol",
_ => "User not found",
}
.to_string()
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (tab, set_tab) = create_signal(cx, 0);
// this will reload every time `tab` changes
let user_data = create_resource(cx, tab, |tab| async move { important_api_call(tab).await });
view! { cx,
<div class="buttons">
<button
on:click=move |_| set_tab(0)
class:selected=move || tab() == 0
>
"Tab A"
</button>
<button
on:click=move |_| set_tab(1)
class:selected=move || tab() == 1
>
"Tab B"
</button>
<button
on:click=move |_| set_tab(2)
class:selected=move || tab() == 2
>
"Tab C"
</button>
{move || if user_data.loading().get() {
"Loading..."
} else {
""
}}
</div>
<Transition
// the fallback will show initially
// on subsequent reloads, the current child will
// continue showing
fallback=move || view! { cx, <p>"Loading..."</p> }
>
<p>
{move || user_data.read(cx)}
</p>
</Transition>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -11,7 +11,7 @@ Actions and resources seem similar, but they represent fundamentally different t
Say we have some `async` function we want to run.
```rust
async fn add_todo(new_title: &str) -> Uuid {
async fn add_todo_request(new_title: &str) -> Uuid {
/* do some stuff on the server to add a new todo */
}
```
@@ -41,16 +41,16 @@ async fn add_todo(new_title: &str) -> Uuid {
So in this case, all we need to do to create an action is
```rust
let add_todo = create_action(cx, |input: &String| {
let add_todo_action = create_action(cx, |input: &String| {
let input = input.to_owned();
async move { add_todo(&input).await }
async move { add_todo_request(&input).await }
});
```
Rather than calling `add_todo` directly, well call it with `.dispatch()`, as in
Rather than calling `add_todo_action` directly, well call it with `.dispatch()`, as in
```rust
add_todo.dispatch("Some value".to_string());
add_todo_action.dispatch("Some value".to_string());
```
You can do this from an event listener, a timeout, or anywhere; because `.dispatch()` isnt an `async` function, it can be called from a synchronous context.
@@ -58,9 +58,9 @@ You can do this from an event listener, a timeout, or anywhere; because `.dispat
Actions provide access to a few signals that synchronize between the asynchronous action youre calling and the synchronous reactive system:
```rust
let submitted = add_todo.input(); // RwSignal<Option<String>>
let pending = add_todo.pending(); // ReadSignal<bool>
let todo_id = add_todo.value(); // RwSignal<Option<Uuid>>
let submitted = add_todo_action.input(); // RwSignal<Option<String>>
let pending = add_todo_action.pending(); // ReadSignal<bool>
let todo_id = add_todo_action.value(); // RwSignal<Option<Uuid>>
```
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
@@ -73,7 +73,7 @@ view! { cx,
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
let input = input_ref.get().expect("input to exist");
add_todo.dispatch(input.value());
add_todo_action.dispatch(input.value());
}
>
<label>
@@ -94,3 +94,83 @@ Now, theres a chance this all seems a little over-complicated, or maybe too r
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use gloo_timers::future::TimeoutFuture;
use leptos::{html::Input, *};
use uuid::Uuid;
// Here we define an async function
// This could be anything: a network request, database read, etc.
// Think of it as a mutation: some imperative async action you run,
// whereas a resource would be some async data you load
async fn add_todo(text: &str) -> Uuid {
_ = text;
// fake a one-second delay
TimeoutFuture::new(1_000).await;
// pretend this is a post ID or something
Uuid::new_v4()
}
#[component]
fn App(cx: Scope) -> impl IntoView {
// an action takes an async function with single argument
// it can be a simple type, a struct, or ()
let add_todo = create_action(cx, |input: &String| {
// the input is a reference, but we need the Future to own it
// this is important: we need to clone and move into the Future
// so it has a 'static lifetime
let input = input.to_owned();
async move { add_todo(&input).await }
});
// actions provide a bunch of synchronous, reactive variables
// that tell us different things about the state of the action
let submitted = add_todo.input();
let pending = add_todo.pending();
let todo_id = add_todo.value();
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.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>
<p>{move || pending().then(|| "Loading...")}</p>
<p>
"Submitted: "
<code>{move || format!("{:#?}", submitted())}</code>
</p>
<p>
"Pending: "
<code>{move || format!("{:#?}", pending())}</code>
</p>
<p>
"Todo ID: "
<code>{move || format!("{:#?}", todo_id())}</code>
</p>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -29,9 +29,9 @@ where
}
```
This is pretty straightforward: when the user is logged in, we want to show `children`. Until if the user is not logged in, we want to show `fallback`. And while were waiting to find out, we just render `()`, i.e., nothing.
This is pretty straightforward: when the user is logged in, we want to show `children`. If the user is not logged in, we want to show `fallback`. And while were waiting to find out, we just render `()`, i.e., nothing.
In other words, we want to pass the children of `<WhenLoaded/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
In other words, we want to pass the children of `<LoggedIn/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
This wont compile.

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

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
# Responding to Changes with `create_effect`
Believe it or not, weve made it this far without having mentioned half of the reactive system: effects.
Weve made it this far without having mentioned half of the reactive system: effects.
Leptos is built on a fine-grained reactive system, which means that individual reactive values (“signals,” sometimes known as observables) trigger rerunning the code that reacts to them (“effects,” sometimes known as observers). These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as theres no observable value to subscribe to.
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.
@@ -26,7 +28,7 @@ If youre familiar with a framework like React, you might notice one key diffe
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
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.
@@ -110,3 +112,195 @@ Every time `count` is updated, this effect wil rerun. This is what allows reacti
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::html::Input;
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
// Just making a visible log here
// You can ignore this...
let log = create_rw_signal::<Vec<String>>(cx, vec![]);
let logged = move || log().join("\n");
provide_context(cx, log);
view! { cx,
<CreateAnEffect/>
<pre>{logged}</pre>
}
}
#[component]
fn CreateAnEffect(cx: Scope) -> impl IntoView {
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()
},
)
});
view! { cx,
<h1><code>"create_effect"</code> " Version"</h1>
<form>
<label>
"First Name"
<input type="text" name="first" prop:value=first
on:change=move |ev| set_first(event_target_value(&ev))
/>
</label>
<label>
"Last Name"
<input type="text" name="last" prop:value=last
on:change=move |ev| set_last(event_target_value(&ev))
/>
</label>
<label>
"Show Last Name"
<input type="checkbox" name="use_last" prop:checked=use_last
on:change=move |ev| set_use_last(event_target_checked(&ev))
/>
</label>
</form>
}
}
#[component]
fn ManualVersion(cx: Scope) -> impl IntoView {
let first = create_node_ref::<Input>(cx);
let last = create_node_ref::<Input>(cx);
let use_last = create_node_ref::<Input>(cx);
let mut prev_name = String::new();
let on_change = move |_| {
log(cx, " listener");
let first = first.get().unwrap();
let last = last.get().unwrap();
let use_last = use_last.get().unwrap();
let this_one = if use_last.checked() {
format!("{} {}", first.value(), last.value())
} else {
first.value()
};
if this_one != prev_name {
log(cx, &this_one);
prev_name = this_one;
}
};
view! { cx,
<h1>"Manual Version"</h1>
<form on:change=on_change>
<label>
"First Name"
<input type="text" name="first"
node_ref=first
/>
</label>
<label>
"Last Name"
<input type="text" name="last"
node_ref=last
/>
</label>
<label>
"Show Last Name"
<input type="checkbox" name="use_last"
checked
node_ref=use_last
/>
</label>
</form>
}
}
#[component]
fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
let (my_value, set_my_value) = create_signal(cx, String::new());
// Don't do this.
/*let (my_optional_value, set_optional_my_value) = create_signal(cx, Option::<String>::None);
create_effect(cx, move |_| {
if !my_value.get().is_empty() {
set_optional_my_value(Some(my_value.get()));
} else {
set_optional_my_value(None);
}
});*/
// Do this
let my_optional_value =
move || (!my_value.with(String::is_empty)).then(|| Some(my_value.get()));
view! { cx,
<input
prop:value=my_value
on:input= move |ev| set_my_value(event_target_value(&ev))
/>
<p>
<code>"my_optional_value"</code>
" is "
<code>
<Show
when=move || my_optional_value().is_some()
fallback=|cx| view! { cx, "None" }
>
"Some(\"" {my_optional_value().unwrap()} "\")"
</Show>
</code>
</p>
}
}
/*#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in
cx: Scope,
/// The components Show wraps
children: Box<dyn Fn(Scope) -> Fragment>,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false
fallback: F,
) -> impl IntoView
where
W: Fn() -> bool + 'static,
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
let memoized_when = create_memo(cx, move |_| when());
move || match memoized_when.get() {
true => children(cx).into_view(cx),
false => fallback(cx).into_view(cx),
}
}*/
fn log(cx: Scope, msg: impl std::fmt::Display) {
let log = use_context::<RwSignal<Vec<String>>>(cx).unwrap();
log.update(|log| log.push(msg.to_string()));
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

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

@@ -68,7 +68,7 @@ pub fn SimpleCounter(cx: Scope) -> impl IntoView {
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.
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in response to a change.
So remember two things:

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

@@ -24,7 +24,7 @@ use leptos_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?
> You shouldnt try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which one decides what to do when the URL changes?
Lets start with a simple `<App/>` component using the router:
@@ -34,7 +34,7 @@ use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! {
view! { cx,
<Router>
<nav>
/* ... */
@@ -59,7 +59,7 @@ use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! {
view! { cx,
<Router>
<nav>
/* ... */
@@ -87,15 +87,17 @@ 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/> }/>
<Route path="/" view=Home/>
<Route path="/users" view=Users/>
<Route path="/users/:id" view=UserProfile/>
<Route path="/*any" view=NotFound/>
</Routes>
```
> The router scores each route to see how good a match it is, so you can define your routes in any order.
> `view` takes a `Fn(Scope) -> impl IntoView`. If a component has no props, it is a function that takes `Scope` and returns `impl IntoView`, so it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|cx| view! { cx, <Home/> }`.
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.
Note that you can define your routes in any order. The router scores each route to see how good a match it is, rather than simply trying to match them top to bottom.
Simple enough?

View File

@@ -4,10 +4,10 @@ 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 /> }/>
<Route path="/" view=Home/>
<Route path="/users" view=Users/>
<Route path="/users/:id" view=UserProfile/>
<Route path="/*any" view=NotFound/>
</Routes>
```
@@ -17,11 +17,11 @@ 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 path="/" view=Home/>
<Route path="/users" view=Users>
<Route path=":id" view=UserProfile/>
</Route>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
<Route path="/*any" view=NotFound/>
</Routes>
```
@@ -39,8 +39,8 @@ 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 /> }/>
<Route path="/users" view=Users/>
<Route path="/users/:id" view=UserProfile/>
</Routes>
```
@@ -53,8 +53,8 @@ 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 path="/users" view=Users>
<Route path=":id" view=UserProfile/>
</Route>
</Routes>
```
@@ -68,9 +68,9 @@ 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 path="/users" view=Users>
<Route path=":id" view=UserProfile/>
<Route path="" view=NoUser/>
</Route>
</Routes>
```
@@ -94,8 +94,8 @@ 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="/contacts" view=ContactList>
<Route path=":id" view=ContactInfo/>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
}/>
@@ -107,11 +107,11 @@ You can go even deeper. Say you want to have tabs for each contacts address,
```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 path="/contacts" view=ContactList>
<Route path=":id" view=ContactInfo>
<Route path="" view=EmailAndPhone/>
<Route path="address" view=Address/>
<Route path="messages" view=Messages/>
</Route>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
@@ -124,7 +124,7 @@ You can go even deeper. Say you want to have tabs for each contacts address,
## `<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 at at the end of the parent component” is not a great answer.
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:
@@ -170,3 +170,118 @@ In fact, in this case, we dont even need to rerender the `<Contact/>` compone
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
// because it's outside the <Routes/>
// note: we can just use normal <a> tags
// and the router will use client-side navigation
<nav>
<h2>"Navigation"</h2>
<a href="/">"Home"</a>
<a href="/contacts">"Contacts"</a>
</nav>
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=ContactList
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
<h3>"Contacts"</h3>
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! { cx,
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -36,10 +36,18 @@ struct ContactSearch {
```
> 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.
>
> If you are not using the `nightly` feature, you will get the error
>
> ```
> no function or associated item named `into_param` found for struct `std::string::String` in the current scope
> ```
>
> At the moment, supporting both `T: FromStr` and `Option<T>` for typed params requires a nightly feature. You can fix this by simply changing the struct to use `q: Option<String>` instead of `q: String`.
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
The typed versions return `Memo<Result<T>, _>`. Its a Memo so it reacts to changes in the URL. Its a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
The typed versions return `Memo<Result<T, _>>`. Its a Memo so it reacts to changes in the URL. Its a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
```rust
let params = use_params::<ContactParams>(cx);
@@ -70,10 +78,126 @@ let id = move || {
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 replacing or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
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 explain them all yet.
> 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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
// because it's outside the <Routes/>
// note: we can just use normal <a> tags
// and the router will use client-side navigation
<nav>
<h2>"Navigation"</h2>
<a href="/">"Home"</a>
<a href="/contacts">"Contacts"</a>
</nav>
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=ContactList
>
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
<h3>"Contacts"</h3>
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! { cx,
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -11,6 +11,8 @@ The router will bail out of handling an `<a>` click under a number of situations
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.
> This also means that if you need to opt out of client-side routing, you can do so easily. For example, if you have a link to another page on the same domain, but which isnt part of your Leptos app, you can just use `<a rel="external">` to tell the router it isnt something it can handle.
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.
@@ -21,3 +23,119 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
// because it's outside the <Routes/>
// note: we can just use normal <a> tags
// and the router will use client-side navigation
<nav>
<h2>"Navigation"</h2>
<a href="/">"Home"</a>
<a href="/contacts">"Contacts"</a>
</nav>
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=ContactList
>
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
<h3>"Contacts"</h3>
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! { cx,
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -1,12 +1,12 @@
# The `<Form/>` Component
Links and forms sometimes seem completely unrelated. But in fact, they work in very similar ways.
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.
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.
@@ -65,3 +65,117 @@ Youll notice that this version drops the `Submit` button. Instead, we add an
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1><code>"<Form/>"</code></h1>
<main>
<Routes>
<Route path="" view=FormExample/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
// reactive access to URL query
let query = use_query_map(cx);
let name = move || query().get("name").cloned().unwrap_or_default();
let number = move || query().get("number").cloned().unwrap_or_default();
let select = move || query().get("select").cloned().unwrap_or_default();
view! { cx,
// read out the URL query strings
<table>
<tr>
<td><code>"name"</code></td>
<td>{name}</td>
</tr>
<tr>
<td><code>"number"</code></td>
<td>{number}</td>
</tr>
<tr>
<td><code>"select"</code></td>
<td>{select}</td>
</tr>
</table>
// <Form/> will navigate whenever submitted
<h2>"Manual Submission"</h2>
<Form method="GET" action="">
// input names determine query string key
<input type="text" name="name" value=name/>
<input type="number" name="number" value=number/>
<select name="select">
// `selected` will set which starts as selected
<option selected=move || select() == "A">
"A"
</option>
<option selected=move || select() == "B">
"B"
</option>
<option selected=move || select() == "C">
"C"
</option>
</select>
// submitting should cause a client-side
// navigation, not a full reload
<input type="submit"/>
</Form>
// This <Form/> uses some JavaScript to submit
// on every input
<h2>"Automatic Submission"</h2>
<Form method="GET" action="">
<input
type="text"
name="name"
value=name
// this oninput attribute will cause the
// form to submit on every input to the field
oninput="this.form.requestSubmit()"
/>
<input
type="number"
name="number"
value=number
oninput="this.form.requestSubmit()"
/>
<select name="select"
onchange="this.form.requestSubmit()"
>
<option selected=move || select() == "A">
"A"
</option>
<option selected=move || select() == "B">
"B"
</option>
<option selected=move || select() == "C">
"C"
</option>
</select>
// submitting should cause a client-side
// navigation, not a full reload
<input type="submit"/>
</Form>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

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

View File

@@ -43,15 +43,6 @@ pub fn BusyButton(cx: Scope) -> impl IntoView {
</button>
}
}
// somewhere in main.rs
fn main() {
// ...
AddTodo::register();
// ...
}
```
Youll notice a couple things here right away:
@@ -75,7 +66,7 @@ move |_| {
There are a few things to note about the way you define a server function, too.
- Server functions are created by using the [`#[server]` macro](https://docs.rs/leptos_server/latest/leptos_server/index.html#server) to annotate a top-level function, which can be defined anywhere.
- We provide the macro a type name. The type name is used to register the server function (in `main.rs`), and its used internally as a container to hold, serialize, and deserialize the arguments.
- We provide the macro a 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`.
@@ -106,6 +97,14 @@ In other words, you have two choices:
**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.
@@ -114,7 +113,7 @@ Server functions are a cool technology, but its very important to remember. *
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:
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](https://leptos-rs.github.io/leptos/async/index.html). 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.

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_axum/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 you 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`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract.html) 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

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

View File

@@ -74,11 +74,11 @@ In other words, if this is being compiled to WASM, it has three items; otherwise
When I load the page in the browser, I see nothing. If I open the console I see a bunch of warnings:
```
element with id 0-0-1 not found, ignoring it for hydration
element with id 0-0-2 not found, ignoring it for hydration
element with id 0-0-3 not found, ignoring it for hydration
component with id _0-0-4c not found, ignoring it for hydration
component with id _0-0-4o not found, ignoring it for hydration
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.
@@ -87,6 +87,56 @@ The WASM version of your app, running in the browser, expects to find three item
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.

View File

@@ -76,7 +76,7 @@ wasm-pack test --firefox
### 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).
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/web.rs).
First, we set up the testing environment.

View File

@@ -28,7 +28,7 @@ fn App(cx: Scope) -> impl IntoView {
view! { cx,
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
set_count(3);
}
>
"Click me: "
@@ -78,8 +78,7 @@ This returns a `(getter, setter)` tuple. To access the current value, youll
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
current value, youll call `set_count.set(...)` (or `set_count(...)`).
> `.get()` clones the value and `.set()` overwrites it. In many cases, its more
> efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if youd like to learn more about those trade-offs at this point.
> `.get()` clones the value and `.set()` overwrites it. In many cases, its more efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if youd like to learn more about those trade-offs at this point.
## The View
@@ -140,10 +139,10 @@ view! { cx,
Remember—and this is _very important_—only functions are reactive. This means that
`{count}` and `{count()}` do very different things in your view. `{count}` passes
in a function, telling the framework to update the view every time `count` changes.
`{count()}` access the value of `count` once, and passes an `i32` into the view,
`{count()}` accesses 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 replacing “set this value to 3” with “increment this value by 1”:
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 |_| {
@@ -161,3 +160,67 @@ Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer de
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// The #[component] macro marks a function as a reusable component
// Components are the building blocks of your user interface
// They define a reusable unit of behavior
#[component]
fn App(cx: Scope) -> impl IntoView {
// here we create a reactive signal
// and get a (getter, setter) pair
// signals are the basic unit of change in the framework
// we'll talk more about them later
let (count, set_count) = create_signal(cx, 0);
// the `view` macro is how we define the user interface
// it uses an HTML-like format that can accept certain Rust values
view! { cx,
<button
// on:click will run whenever the `click` event fires
// every event handler is defined as `on:{eventname}`
// we're able to move `set_count` into the closure
// because signals are Copy and 'static
on:click=move |_| {
set_count.update(|n| *n += 1);
}
>
// text nodes in RSX should be wrapped in quotes,
// like a normal Rust string
"Click me"
</button>
<p>
<strong>"Reactive: "</strong>
// you can insert Rust expressions as values in the DOM
// by wrapping them in curly braces
// if you pass in a function, it will reactively update
{move || count.get()}
</p>
<p>
<strong>"Reactive shorthand: "</strong>
// signals are functions, so we can remove the wrapping closure
{count}
</p>
<p>
<strong>"Not reactive: "</strong>
// NOTE: if you write {count()}, this will *not* be reactive
// it simply gets the value of count once
{count()}
</p>
}
}
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```

View File

@@ -152,3 +152,67 @@ places in your application with minimal overhead.
[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>
<details>
<summary>Code Sandbox Source</summary>
```rust
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
// a "derived signal" is a function that accesses other signals
// we can use this to create reactive values that depend on the
// values of one or more other signals
let double_count = move || count() * 2;
view! { cx,
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
}
// the class: syntax reactively updates a single class
// here, we'll set the `red` class when `count` is odd
class:red=move || count() % 2 == 1
>
"Click me"
</button>
// NOTE: self-closing tags like <br> need an explicit /
<br/>
// We'll update this progress bar every time `count` changes
<progress
// static attributes work as in HTML
max="50"
// passing a function to an attribute
// reactively sets that attribute
// signals are functions, so this <=> `move || count.get()`
value=count
>
</progress>
<br/>
// This progress bar will use `double_count`
// so it should move twice as fast!
<progress
max="50"
// derived signals are functions, so they can also
// reactive update the DOM
value=double_count
>
</progress>
<p>"Count: " {count}</p>
<p>"Double Count: " {double_count}</p>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -98,18 +98,6 @@ notice that you can easily tell the difference between an element and a componen
because components always have `PascalCase` names. You pass the `progress` prop
in as if it were an HTML element attribute. Simple.
> ### Important Note
>
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
> is what allows us to have named props, when Rust does not have named function parameters.
> If youre defining a component in one module and importing it into another, make
> sure you include this `ComponentProps` type:
>
> `use progress_bar::{ProgressBar, ProgressBarProps};`
>
> **Note**: This is still true as of `0.2.5`, but the requirement has been removed on `main`
> and will not apply to later versions.
### Reactive and Static Props
Youll notice that throughout this example, `progress` takes a reactive
@@ -231,9 +219,25 @@ where
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.
This generic can also be specified inline:
```rust
#[component]
fn ProgressBar<F: Fn() -> i32 + 'static>(
cx: Scope,
#[prop(default = 100)] max: u16,
progress: F,
) -> impl IntoView {
view! { cx,
<progress
max=max
value=progress
/>
}
}
```
> Note that generic component props _cant_ be specified with an `impl` yet (`progress: impl Fn() -> i32 + 'static,`), in part because theyre actually used to generate a `struct ProgressBarProps`, and struct fields cannot be `impl` types. The `#[component]` macro may be further improved in the future to allow inline `impl` generic props.
### `into` Props
@@ -283,6 +287,81 @@ fn App(cx: Scope) -> impl IntoView {
}
```
### Optional Generic Props
Note that you cant specify optional generic props for a component. Lets see what would happen if you try:
```rust,compile_fail
#[component]
fn ProgressBar<F: Fn() -> i32 + 'static>(
cx: Scope,
#[prop(optional)] progress: Option<F>,
) -> impl IntoView {
progress.map(|progress| {
view! { cx,
<progress
max=100
value=progress
/>
}
})
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
<ProgressBar/>
}
}
```
Rust helpfully gives the error
```
xx | <ProgressBar/>
| ^^^^^^^^^^^ cannot infer type of the type parameter `F` declared on the function `ProgressBar`
|
help: consider specifying the generic argument
|
xx | <ProgressBar::<F>/>
| +++++
```
There are just two problems:
1. Leptoss view macro doesnt support specifying a generic on a component with this turbofish syntax.
2. Even if you could, specifying the correct type here is not possible; closures and functions in general are unnameable types. The compiler can display them with a shorthand, but you cant specify them.
However, you can get around this by providing a concrete type using `Box<dyn _>` or `&dyn _`:
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>,
) -> impl IntoView {
progress.map(|progress| {
view! { cx,
<progress
max=100
value=progress
/>
}
})
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
<ProgressBar/>
}
}
```
Because the Rust compiler now knows the concrete type of the prop, and therefore its size in memory even in the `None` case, this compiles fine.
> In this particular case, `&dyn Fn() -> i32` will cause lifetime issues, but in other cases, it may be a possibility.
## Documenting Components
This is one of the least essential but most important sections of this book.
@@ -321,3 +400,77 @@ 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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// Composing different components together is how we build
// user interfaces. Here, we'll define a resuable <ProgressBar/>.
// You'll see how doc comments can be used to document components
// and their properties.
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
// All components take a reactive `Scope` as the first argument
cx: Scope,
// Marks this as an optional prop. It will default to the default
// value of its type, i.e., 0.
#[prop(default = 100)]
/// The maximum value of the progress bar.
max: u16,
// Will run `.into()` on the value passed into the prop.
#[prop(into)]
// `Signal<T>` is a wrapper for several reactive types.
// It can be helpful in component APIs like this, where we
// might want to take any kind of reactive value
/// How much progress should be displayed.
progress: Signal<i32>,
) -> impl IntoView {
view! { cx,
<progress
max={max}
value=progress
/>
<br/>
}
}
#[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>
<br/>
// If you have this open in CodeSandbox or an editor with
// rust-analyzer support, try hovering over `ProgressBar`,
// `max`, or `progress` to see the docs we defined above
<ProgressBar max=50 progress=count/>
// Let's use the default max value on this one
// the default is 100, so it should move half as fast
<ProgressBar progress=count/>
// Signal::derive creates a Signal wrapper from our derived signal
// using double_count means it should move twice as fast
<ProgressBar max=50 progress=Signal::derive(cx, double_count)/>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -106,3 +106,162 @@ 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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// Iteration is a very common task in most applications.
// So how do you take a list of data and render it in the DOM?
// This example will show you the two ways:
// 1) for mostly-static lists, using Rust iterators
// 2) for lists that grow, shrink, or move items, using <For/>
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<h1>"Iteration"</h1>
<h2>"Static List"</h2>
<p>"Use this pattern if the list itself is static."</p>
<StaticList length=5/>
<h2>"Dynamic List"</h2>
<p>"Use this pattern if the rows in your list will change."</p>
<DynamicList initial_length=5/>
}
}
/// A list of counters, without the ability
/// to add or remove any.
#[component]
fn StaticList(
cx: Scope,
/// How many counters to include in this list.
length: usize,
) -> impl IntoView {
// create counter signals that start at incrementing numbers
let counters = (1..=length).map(|idx| create_signal(cx, idx));
// when you have a list that doesn't change, you can
// manipulate it using ordinary Rust iterators
// and collect it into a Vec<_> to insert it into the DOM
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::<Vec<_>>();
// Note that if `counter_buttons` were a reactive list
// and its value changed, this would be very inefficient:
// it would rerender every row every time the list changed.
view! { cx,
<ul>{counter_buttons}</ul>
}
}
/// A list of counters that allows you to add or
/// remove counters.
#[component]
fn DynamicList(
cx: Scope,
/// The number of counters to begin with.
initial_length: usize,
) -> impl IntoView {
// This dynamic list will use the <For/> component.
// <For/> is a keyed list. This means that each row
// has a defined key. If the key does not change, the row
// will not be re-rendered. When the list changes, only
// the minimum number of changes will be made to the DOM.
// `next_counter_id` will let us generate unique IDs
// we do this by simply incrementing the ID by one
// each time we create a counter
let mut next_counter_id = initial_length;
// we generate an initial list as in <StaticList/>
// but this time we include the ID along with the signal
let initial_counters = (0..initial_length)
.map(|id| (id, create_signal(cx, id + 1)))
.collect::<Vec<_>>();
// now we store that initial list in a signal
// this way, we'll be able to modify the list over time,
// adding and removing counters, and it will change reactively
let (counters, set_counters) = create_signal(cx, initial_counters);
let add_counter = move |_| {
// create a signal for the new counter
let sig = create_signal(cx, next_counter_id + 1);
// add this counter to the list of counters
set_counters.update(move |counters| {
// since `.update()` gives us `&mut T`
// we can just use normal Vec methods like `push`
counters.push((next_counter_id, sig))
});
// increment the ID so it's always unique
next_counter_id += 1;
};
view! { cx,
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<ul>
// The <For/> component is central here
// This allows for efficient, key list rendering
<For
// `each` takes any function that returns an iterator
// this should usually be a signal or derived signal
// if it's not reactive, just render a Vec<_> instead of <For/>
each=counters
// the key should be unique and stable for each row
// using an index is usually a bad idea, unless your list
// can only grow, because moving items around inside the list
// means their indices will change and they will all rerender
key=|counter| counter.0
// the view function receives each item from your `each` iterator
// and returns a view
view=move |cx, (id, (count, set_count))| {
view! { cx,
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
>
{count}
</button>
<button
on:click=move |_| {
set_counters.update(|counters| {
counters.retain(|(counter_id, _)| counter_id != &id)
});
}
>
"Remove"
</button>
</li>
}
}
/>
</ul>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -112,3 +112,112 @@ The view should be pretty self-explanatory by now. Note two things:
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::{ev::SubmitEvent, *};
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<h2>"Controlled Component"</h2>
<ControlledComponent/>
<h2>"Uncontrolled Component"</h2>
<UncontrolledComponent/>
}
}
#[component]
fn ControlledComponent(cx: Scope) -> impl IntoView {
// create a signal to hold the value
let (name, set_name) = create_signal(cx, "Controlled".to_string());
view! { cx,
<input type="text"
// fire an event whenever the input changes
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.
//
// IMPORTANT: the `value` *attribute* only sets the
// initial value, until you have made a change.
// The `value` *property* sets the current value.
// This is a quirk of the DOM; I didn't invent it.
// Other frameworks gloss this over; I think it's
// more important to give you access to the browser
// as it really works.
//
// tl;dr: use prop:value for form inputs
prop:value=name
/>
<p>"Name is: " {name}</p>
}
}
#[component]
fn UncontrolledComponent(cx: Scope) -> impl IntoView {
// import the type for <input>
use leptos::html::Input;
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
// we'll use a NodeRef to store a reference to the input element
// this will be filled when the element is created
let input_element: NodeRef<Input> = create_node_ref(cx);
// fires when the form `submit` event happens
// this will store the value of the <input> in our signal
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);
};
view! { cx,
<form on:submit=on_submit>
<input type="text"
// here, we use the `value` *attribute* to set only
// the initial value, letting the browser maintain
// the state after that
value=name
// store a reference to this input in `input_element`
node_ref=input_element
/>
<input type="submit" value="Submit"/>
</form>
<p>"Name is: " {name}</p>
}
}
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -198,7 +198,7 @@ let (value, set_value) = create_signal(cx, 0);
view! { cx,
<Show
when=move || value() > 5
when=move || { value() > 5 }
fallback=|cx| view! { cx, <Small/> }
>
<Big/>
@@ -285,3 +285,100 @@ view! { cx,
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
let is_odd = move || value() & 1 == 1;
let odd_text = move || if is_odd() { Some("How odd!") } else { None };
view! { cx,
<h1>"Control Flow"</h1>
// Simple UI to update and show a value
<button on:click=move |_| set_value.update(|n| *n += 1)>
"+1"
</button>
<p>"Value is: " {value}</p>
<hr/>
<h2><code>"Option<T>"</code></h2>
// For any `T` that implements `IntoView`,
// so does `Option<T>`
<p>{odd_text}</p>
// This means you can use `Option` methods on it
<p>{move || odd_text().map(|text| text.len())}</p>
<h2>"Conditional Logic"</h2>
// You can do dynamic conditional if-then-else
// logic in several ways
//
// a. An "if" expression in a function
// This will simply re-render every time the value
// changes, which makes it good for lightweight UI
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
// b. Toggling some kind of class
// This is smart for an element that's going to
// toggled often, because it doesn't destroy
// it in between states
// (you can find the `hidden` class in `index.html`)
<p class:hidden=is_odd>"Appears if even."</p>
// c. The <Show/> component
// This only renders the fallback and the child
// once, lazily, and toggles between them when
// needed. This makes it more efficient in many cases
// than a {move || if ...} block
<Show when=is_odd
fallback=|cx| view! { cx, <p>"Even steven"</p> }
>
<p>"Oddment"</p>
</Show>
// d. Because `bool::then()` converts a `bool` to
// `Option`, you can use it to create a show/hide toggled
{move || is_odd().then(|| view! { cx, <p>"Oddity!"</p> })}
<h2>"Converting between Types"</h2>
// e. Note: if branches return different types,
// you can convert between them with
// `.into_any()` (for different HTML element types)
// or `.into_view(cx)` (for all view types)
{move || match is_odd() {
true if value() == 1 => {
// <pre> returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// <p> returns HtmlElement<P>
// so we convert into a more generic type
view! { cx, <p>"Two"</p> }.into_any()
}
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
}}
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -19,7 +19,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
view! { cx,
<label>
"Type a number (or not!)"
<input type="number" on:input=on_input/>
<input on:input=on_input/>
<p>
"You entered "
<strong>{value}</strong>
@@ -69,7 +69,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input=on_input/>
<input on:input=on_input/>
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
@@ -113,3 +113,64 @@ 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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
#[component]
fn App(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,
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input=on_input/>
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
<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::<Vec<_>>()
}
</ul>
</div>
}
>
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
<strong>{value}</strong>
</p>
</ErrorBoundary>
</label>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -117,7 +117,7 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ButtonC<F>(cx: Scope) -> impl IntoView {
pub fn ButtonC(cx: Scope) -> impl IntoView {
view! { cx,
<button>"Toggle"</button>
}
@@ -288,3 +288,150 @@ 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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::{ev::MouseEvent, *};
// This highlights four different ways that child components can communicate
// with their parent:
// 1) <ButtonA/>: passing a WriteSignal as one of the child component props,
// for the child component to write into and the parent to read
// 2) <ButtonB/>: passing a closure as one of the child component props, for
// the child component to call
// 3) <ButtonC/>: adding an `on:` event listener to a component
// 4) <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
#[derive(Copy, Clone)]
struct SmallcapsContext(WriteSignal<bool>);
#[component]
pub fn App(cx: Scope) -> impl IntoView {
// just some signals to toggle three classes on our <p>
let (red, set_red) = create_signal(cx, false);
let (right, set_right) = create_signal(cx, false);
let (italics, set_italics) = create_signal(cx, false);
let (smallcaps, set_smallcaps) = create_signal(cx, false);
// the newtype pattern isn't *necessary* here but is a good practice
// it avoids confusion with other possible future `WriteSignal<bool>` contexts
// and makes it easier to refer to it in ButtonC
provide_context(cx, SmallcapsContext(set_smallcaps));
view! {
cx,
<main>
<p
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
class:red=red
class:right=right
class:italics=italics
class:smallcaps=smallcaps
>
"Lorem ipsum sit dolor amet."
</p>
// Button A: pass the signal setter
<ButtonA setter=set_red/>
// Button B: pass a closure
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
// Button B: use a regular event listener
// setting an event listener on a component like this applies it
// to each of the top-level elements the component returns
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
// Button D gets its setter from context rather than props
<ButtonD/>
</main>
}
}
/// Button A receives a signal setter and updates the signal itself
#[component]
pub fn ButtonA(
cx: Scope,
/// Signal that will be toggled when the button is clicked.
setter: WriteSignal<bool>,
) -> impl IntoView {
view! {
cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
"Toggle Red"
</button>
}
}
/// Button B receives a closure
#[component]
pub fn ButtonB<F>(
cx: Scope,
/// Callback that will be invoked when the button is clicked.
on_click: F,
) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
{
view! {
cx,
<button
on:click=on_click
>
"Toggle Right"
</button>
}
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(MouseEvent) + 'static
// and save you from typing out the generic
// the component macro actually expands to define a
//
// struct ButtonBProps<F> where F: Fn(MouseEvent) + 'static {
// on_click: F
// }
//
// this is what allows us to have named props in our component invocation,
// instead of an ordered list of function arguments
// if Rust ever had named function arguments we could drop this requirement
}
/// Button C is a dummy: it renders a button but doesn't handle
/// its click. Instead, the parent component adds an event listener.
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
view! {
cx,
<button>
"Toggle Italics"
</button>
}
}
/// Button D is very similar to Button A, but instead of passing the setter as a prop
/// we get it from the context
#[component]
pub fn ButtonD(cx: Scope) -> impl IntoView {
let setter = use_context::<SmallcapsContext>(cx).unwrap().0;
view! {
cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
"Toggle Small Caps"
</button>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -115,14 +115,118 @@ Calling it like this will create a list:
```rust
view! { cx,
<WrappedChildren>
<WrapsChildren>
"A"
"B"
"C"
</WrappedChildren>
</WrapsChildren>
}
```
[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>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// Often, you want to pass some kind of child view to another
// component. There are two basic patterns for doing this:
// - "render props": creating a component prop that takes a function
// that creates a view
// - the `children` prop: a special property that contains content
// passed as the children of a component in your view, not as a
// property
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (items, set_items) = create_signal(cx, vec![0, 1, 2]);
let render_prop = move || {
// items.with(...) reacts to the value without cloning
// by applying a function. Here, we pass the `len` method
// on a `Vec<_>` directly
let len = move || items.with(Vec::len);
view! { cx,
<p>"Length: " {len}</p>
}
};
view! { cx,
// This component just displays the two kinds of children,
// embedding them in some other markup
<TakesChildren
// for component props, you can shorthand
// `render_prop=render_prop` => `render_prop`
// (this doesn't work for HTML element attributes)
render_prop
>
// these look just like the children of an HTML element
<p>"Here's a child."</p>
<p>"Here's another child."</p>
</TakesChildren>
<hr/>
// This component actually iterates over and wraps the children
<WrapsChildren>
<p>"Here's a child."</p>
<p>"Here's another child."</p>
</WrapsChildren>
}
}
/// Displays a `render_prop` and some children within markup.
#[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
/// this is an alias for `Box<dyn FnOnce(Scope) -> Fragment>`
/// ... aren't you glad we named it `Children` instead?
children: Children,
) -> impl IntoView
where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
<h1><code>"<TakesChildren/>"</code></h1>
<h2>"Render Prop"</h2>
{render_prop()}
<hr/>
<h2>"Children"</h2>
{children(cx)}
}
}
/// Wraps each child in an `<li>` and embeds them in a `<ul>`.
#[component]
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
// children(cx) returns a `Fragment`, which has a
// `nodes` field that contains a Vec<View>
// this means we can iterate over the children
// to create something new!
let children = children(cx)
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect::<Vec<_>>();
view! { cx,
<h1><code>"<WrapsChildren/>"</code></h1>
// wrap our wrapped children in a UL
<ul>{children}</ul>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -1,57 +1,121 @@
extend = [{ path = "./cargo-make/common.toml" }]
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 = [
"animated_show",
"counter",
"counter_isomorphic",
"counters",
"counters_stable",
"counter_url_query,
"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.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify", "verify", "post-verify"]
[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"
'''
[tasks.verify]
description = "Run all quality checks and tests"
dependencies = ["check-style", "test-unit-and-web"]
[tasks.test-info]
workspace = false
description = "report ci test runners for each example - Option [all]"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
ITALIC="\e[3m"
YELLOW="\e[0;33m"
RESET="\e[0m"
[tasks.test-unit-and-web]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
echo
echo "${YELLOW}CI test runners by example...${RESET}"
echo
[tasks.pre-verify]
examples=$(ls |
grep -v README.md |
grep -v Makefile.toml |
grep -v cargo-make |
grep -v gtk |
sort -u |
awk '{print $0 ", "}')
[tasks.post-verify]
dependencies = ["clean-all"]
example_root_dir=$(pwd)
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test", "web-test", "post-web-test"]
for example_dir in $examples
do
clean_name=$(echo $example_dir | sed 's%,%%')
cd $clean_name
[tasks.pre-web-test]
c_tests=$(grep -rl --fixed-strings "#[test]" | wc -l)
rs_tests=$(grep -rl --fixed-strings "#[rstest]" | wc -l)
w_configs=$(grep -rl "\/wasm-test.toml\"" | wc -l)
pw_configs=$(grep -rl "\/playwright-test.toml\"" | wc -l)
cl_configs=$(grep -rl "\/cargo-leptos-test.toml\"" | wc -l)
[tasks.web-test]
test_runner=
[tasks.post-web-test]
if [ $c_tests -gt 0 ]; then
test_runner="-C"
fi
if [ $rs_tests -gt 0 ]; then
test_runner=$test_runner"-R"
fi
if [ $w_configs -gt 0 ]; then
test_runner=$test_runner"-W"
fi
if [ $pw_configs -gt 0 ]; then
test_runner=$test_runner"-P"
fi
if [ $cl_configs -gt 0 ]; then
test_runner=$test_runner"-L"
fi
if [ ! -z "$1" ]; then
# Show all examples
echo "$clean_name ${BOLD}${test_runner}${RESET}"
elif [ ! -z $test_runner ]; then
# Filter out examples that do not run tests in `ci`
echo "$clean_name ${BOLD}${test_runner}${RESET}"
fi
cd $example_root_dir
done
echo
echo "${ITALIC}Test Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, R = RS Test, W = WASM Test${RESET}"
echo
'''

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,14 @@
[package]
name = "animated-show"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

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

View File

@@ -0,0 +1,9 @@
# `<AnimatedShow>` combined with CSS animations
This is a very simple example of the `<AnimatedShow>` component.
This component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the
component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
CSS.
Just execute `trunk serve` to start the demo.

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
<style>
.hover-me {
width: 100px;
margin: 1rem;
padding: 1rem;
text-align: center;
cursor: pointer;
border: 1px solid grey;
}
.here-i-am {
width: 100px;
margin: 1rem;
padding: 1rem;
text-align: center;
color: white;
font-weight: bold;
background: black;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
.fade-in-1000 {
animation: 1000ms fade-in forwards;
}
.fade-out-1000 {
animation: 1000ms fade-out forwards;
}
</style>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,34 @@
use core::time::Duration;
use leptos::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let show = create_rw_signal(cx, false);
// the CSS classes in this example are just written directly inside the `index.html`
view! { cx,
<div
class="hover-me"
on:mouseenter=move |_| show.set(true)
on:mouseleave=move |_| show.set(false)
>
"Hover Me"
</div>
<AnimatedShow
when=show
// optional CSS class which will be applied if `when == true`
show_class="fade-in-1000"
// optional CSS class which will be applied if `when == false` and before the
// `hide_delay` starts -> makes CSS unmount animations really easy
hide_class="fade-out-1000"
// the given unmount delay which should match your unmount animation duration
hide_delay=Duration::from_millis(1000)
>
// provide any `Children` inside here
<div class="here-i-am">
"Here I Am!"
</div>
</AnimatedShow>
}
}

View File

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

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

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

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

@@ -1,87 +0,0 @@
[env]
END2END_DIR = "end2end"
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
args = ["fmt", "--", "--check", "--config-path", "../../"]
[tasks.verify-local]
description = "Run all quality checks and tests from an example directory"
dependencies = ["check-style", "test-local"]
[tasks.test-local]
description = "Run all tests from an example directory"
dependencies = ["test", "web-test"]
[tasks.clean-cargo]
description = "Runs the cargo clean command."
category = "Cleanup"
command = "cargo"
args = ["clean"]
[tasks.clean-trunk]
description = "Runs the trunk clean command."
category = "Cleanup"
command = "trunk"
args = ["clean"]
[tasks.clean-node_modules]
description = "Delete all node_modules directories"
category = "Cleanup"
script = '''
find . -type d -name node_modules | xargs rm -rf
'''
[tasks.clean-playwright]
description = "Delete playwright directories"
category = "Cleanup"
cwd = "${END2END_DIR}"
command = "rm"
args = ["-rf", "playwright", "playwright/.cache", "test-results"]
[tasks.clean-all]
description = "Delete all temporary directories"
category = "Cleanup"
dependencies = ["clean-cargo"]
[tasks.wasm-web-test]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.cargo-leptos-e2e]
description = "Runs end to end tests with cargo leptos"
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.setup]
description = "Setup e2e dependencies"
cwd = "${END2END_DIR}"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
if command -v pnpm; then
pnpm install
elif command -v npm; then
npm install
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
'''
[tasks.auto-setup]
script = '''
if [ ! -d "${END2END_DIR}/node_modules" ]; then
cargo make setup
fi
'''

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
[tasks.test]
env = { RUN_CARGO_TEST = false }
condition = { env_true = ["RUN_CARGO_TEST"] }
[tasks.post-test]
dependencies = ["test-wasm"]
[tasks.test-wasm]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]

View File

@@ -1,5 +0,0 @@
[tasks.web-test]
dependencies = ["wasm-web-test"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-trunk"]

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
extend = [{ path = "../cargo-make/common.toml" }]
extend = [{ path = "../cargo-make/main.toml" }]
[tasks.build]
command = "cargo"

View File

@@ -12,13 +12,6 @@ cfg_if! {
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
pub fn register_server_functions() {
_ = GetServerCount::register();
_ = AdjustServerCount::register();
_ = ClearServerCount::register();
}
}
}
@@ -74,24 +67,10 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<main>
<Routes>
<Route
path=""
view=|cx| {
view! { cx, <Counter/> }
}
/>
<Route
path="form"
view=|cx| {
view! { cx, <FormCounter/> }
}
/>
<Route
path="multi"
view=|cx| {
view! { cx, <MultiuserCounter/> }
}
/>
<Route path="" view=Counter/>
<Route path="form" view=FormCounter/>
<Route path="multi" view=MultiuserCounter/>
<Route path="multi" view=NotFound/>
</Routes>
</main>
</Router>
@@ -182,13 +161,9 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
</p>
<div>
// calling a server function is the same as POSTing to its API URL
// so we can just do that with a form and button
<ActionForm action=clear>
<input type="submit" value="Clear"/>
</ActionForm>
// We can submit named arguments to the server functions
// by including them as input values with the same name
<ActionForm action=adjust>
<input type="hidden" name="delta" value="-1"/>
<input type="hidden" name="msg" value="form value down"/>
@@ -263,3 +238,14 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
</div>
}
}
#[component]
fn NotFound(cx: Scope) -> impl IntoView {
#[cfg(feature = "ssr")]
{
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
}
view! { cx, <h1>"Not Found"</h1> }
}

View File

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

View File

@@ -0,0 +1,20 @@
[package]
name = "counter_url_query"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
leptos_router = { path = "../../router", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-test = "0.3.0"
web-sys = "0.3"

View File

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

View File

@@ -0,0 +1,7 @@
# Leptos Query Counter Example
This example creates a simple counter whose state is persisted and synced in the url with query params.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,39 @@
use leptos::*;
use leptos_router::*;
/// A simple counter component.
///
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleQueryCounter(cx: Scope) -> impl IntoView {
let (count, set_count) = create_query_signal::<i32>(cx, "count");
let clear = move |_| set_count(None);
let decrement = move |_| set_count(Some(count().unwrap_or(0) - 1));
let increment = move |_| set_count(Some(count().unwrap_or(0) + 1));
let (msg, set_msg) = create_query_signal::<String>(cx, "message");
let update_msg = move |ev| {
let new_msg = event_target_value(&ev);
if new_msg.is_empty() {
set_msg(None);
} else {
set_msg(Some(new_msg));
}
};
view! { cx,
<div>
<button on:click=clear>"Clear"</button>
<button on:click=decrement>"-1"</button>
<span>"Value: " {move || count().unwrap_or(0)} "!"</span>
<button on:click=increment>"+1"</button>
<br />
<input
prop:value=move || msg().unwrap_or_default()
on:input=update_msg
/>
</div>
}
}

View File

@@ -0,0 +1,17 @@
use counter_url_query::SimpleQueryCounter;
use leptos::*;
use leptos_router::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
<Router>
<Routes>
<Route path="" view=SimpleQueryCounter />
</Routes>
</Router>
}
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

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