Compare commits

...

319 Commits
1408 ... v0.5.2

Author SHA1 Message Date
Greg Johnston
5f53a1459e v0.5.2 2023-10-24 21:03:29 -04:00
Greg Johnston
379623d548 chore: fix SSR tests (#1943) 2023-10-24 17:53:45 -04:00
Greg Johnston
db1113e5b3 fix: use separate key in hydration ID for router outlets (closes #1909) (#1939) 2023-10-24 15:42:30 -04:00
Greg Johnston
d943a50df1 fix: misaligned </head> tags in streaming responses (closes #1930) (#1932) 2023-10-24 15:42:07 -04:00
Greg Johnston
eb86899e08 chore: remove wee_alloc to make Dependabot happy (#1938) 2023-10-24 15:41:03 -04:00
Greg Johnston
e2842ede44 chore: fix broken doctests in leptos_reactive 2023-10-24 15:35:16 -04:00
Greg Johnston
fdd4b3d919 chore: cargo fmt 2023-10-24 15:01:33 -04:00
nikhilraojl
7771052db8 docs: clarify docs on resource source signal (#1918) 2023-10-24 14:29:09 -04:00
martin frances
d999ff857d chore: remove cargo doc lint warnings (#1936) 2023-10-24 14:28:01 -04:00
Sadra
30370a55e1 feat: add a slice!() macro (#1867) 2023-10-24 14:27:10 -04:00
koopa
a7330d61b6 feat: add replace prop to Form component (#1923) 2023-10-24 14:24:23 -04:00
Marc-Stefan Cassola
e2f6780de4 docs: added Callback to documentation and examples. (#1926)
* added Callback to documentation and examples.
Also reduced code duplication in Callback implementation.

* added back the closure event callback example
2023-10-24 14:14:51 -04:00
martin frances
05b4f8e617 chore: use .first() [not .get(0)] (#1929) 2023-10-23 21:02:42 -04:00
Greg Johnston
eb888029d1 docs: fix potential panic in 04b_iteration.md 2023-10-22 07:28:06 -04:00
Daniél Kerkmann
3e08486385 feat: Add local attribute for Await (#1922)
This PR adds the ability to use `create_local_resource` instead of
`create_resource`. This will run the create resource locally on the
system and therefore its result type does not need to be `Seriaziable`.

Closes #1567
2023-10-21 18:34:02 -04:00
Greg Johnston
12b0295906 chore: please clippy (#1924) 2023-10-21 18:33:21 -04:00
Greg Johnston
2756327f12 fix: add missing IntoView implementation for Oco<'static, str> 2023-10-21 16:02:14 -04:00
Greg Johnston
b8ca8b7849 docs: add chapter on nested reactivity and iteration (#1920) 2023-10-20 15:18:38 -04:00
Greg Johnston
6abdca0597 docs: better document default and wasm features on leptos_axum (closes #1872) (#1883) 2023-10-20 14:57:53 -04:00
Greg Johnston
bf14999eb2 fix: router should still scroll to hash even if path didn't change (closes #1907) (#1917) 2023-10-20 14:57:35 -04:00
Marc-Stefan Cassola
c87328f5cf feat: add directives with use: (#1821) 2023-10-19 16:15:36 -04:00
safx
9a70898b09 feat: optional named arguments for #[server] macro (#1904) 2023-10-19 16:07:43 -04:00
Greg Johnston
4a83ffca6f fix: try_update() and try_set() on Resource should not panic (closes #1915) (#1916) 2023-10-19 15:56:57 -04:00
Saikat Das
319017f03f docs: fix typo (#1910) 2023-10-19 09:00:30 -04:00
Greg Johnston
33e166a462 allow construction by making data public 2023-10-18 19:16:08 -04:00
Greg Johnston
8994154b23 fix: maintain hash when setting query signal (closes #1902) (#1908) 2023-10-17 20:28:57 -04:00
luoxiaozero
7b88df32d1 feat: add a target prop to the <A/> component (#1906) 2023-10-17 20:28:37 -04:00
PianoPrinter
0d6ddfb71e fix: properly handle trailing / in more routes (#1900) 2023-10-17 12:45:22 -04:00
Greg Johnston
4a4e16c206 chore: tweak tracing levels (#1901) 2023-10-17 12:44:37 -04:00
Henry Rovnyak
11f6a5d341 fix: remove Clone bound for SignalWith for Resource (#1895) 2023-10-16 14:37:38 -04:00
Greg Johnston
ad208ec473 fix: bug with client-side routing no longer working due to different origin (#1899) 2023-10-15 20:39:03 -04:00
Ari Seyhun
72ad1d7c68 feat: add new method to NodeRef (#1896) 2023-10-15 19:56:23 -04:00
Quan Hua
d6a9d2efdf docs: update deployment.md (#1898)
cargo-leptos@0.2.0 changes the file structure under target folder
2023-10-15 19:55:36 -04:00
Greg Johnston
8eed999611 fix: properly handle trailing / in splat routes (closes #1764) (#1890) 2023-10-14 08:37:31 -04:00
Azzam S.A
07f2cbfbba examples: rename Tailwind examples (#1875) 2023-10-13 16:20:26 -04:00
ymijorski
fc4dea6839 feat: allow custom attributes on leptos_meta components (#1874) 2023-10-13 15:30:48 -04:00
Greg Johnston
17b3300351 fix: ensure there's no reactive tracking in an on_cleanup (closes #1882) (#1889) 2023-10-12 07:36:07 -04:00
Greg Johnston
f3508cef36 feat: add reasonable fallback behavior for ActionForm in an island (#1888) 2023-10-11 18:59:49 -04:00
Greg Johnston
5c41d20421 Merge pull request #1887 from leptos-rs/1886
Fix failing `cargo make lint` for `hackernews_js_fetch`
2023-10-11 18:59:22 -04:00
Greg Johnston
53e16751a7 chore: clean up style and bring into line with other hackernews examples 2023-10-11 16:45:48 -04:00
Greg Johnston
18f7b56c03 fix: do not force target wasm32 for CI purposes 2023-10-11 16:45:36 -04:00
Greg Johnston
c6f51e6a09 docs: add some missing #[must_use] to avoid accidental () rendering (#1885) 2023-10-11 15:00:27 -04:00
dandante
971fb734de docs: fix Markdown in 02_getting_started.md (#1873) 2023-10-11 12:06:16 -04:00
luoxiaozero
200304402f feat: implement Default for RwSignal and StoredValue (#1877) 2023-10-11 12:05:05 -04:00
Greg Johnston
4baa75ccf0 fix: correctly untrack in .try_with_untracked (closes #1880) (#1881) 2023-10-11 12:04:16 -04:00
Greg Johnston
9af1c7e1a3 fix: hydration ID clash with Suspense > Outlet > Suspense (closes #1863) (#1864) 2023-10-09 16:22:43 -04:00
obioma
a302257129 docs: improvements in book - closes #1845 (#1856) 2023-10-09 16:21:09 -04:00
Marc-Stefan Cassola
4251f6c0f4 feat: add Portal component (#1820) 2023-10-09 16:18:52 -04:00
Markus Kohlhase
c080c2cbca fix: use placeholder in comments for empty component names (#1850) 2023-10-09 16:17:46 -04:00
Tyrone Tudehope
0676348bd4 docs: fix Prop-drilling example in Parent-Child Communication section (#1865) 2023-10-09 16:17:26 -04:00
Tyrone Tudehope
18ad7cde20 docs: Add missing argument name to WrapsChildren component (#1866) 2023-10-09 16:16:59 -04:00
Jesse He
b61d0553a0 docs: remove extra "```rust" and add closing bracket to Testing docs (#1870) 2023-10-09 16:16:43 -04:00
Greg Johnston
2a3b613230 docs: add islands guide/demo to the docs (#1861) 2023-10-07 13:10:49 -04:00
Greg Johnston
0d4862b238 feat: add extractor functions with better API than extract (closes #1755) (#1859) 2023-10-07 13:10:30 -04:00
hiraginoyuki
c7607f6fcc fix: documentation in leptos_reactive::Trigger (#1844) 2023-10-07 10:45:27 -04:00
Artur Corrêa Souza
29216b226f docs: clarify what "once per signal change" means (#1858) 2023-10-07 10:44:06 -04:00
Greg Johnston
32ba0ce4fb add awesome-leptos to README 2023-10-07 10:43:21 -04:00
Greg Johnston
c781b4e1c7 docs: update CodeSandboxes to 0.5 2023-10-06 14:20:01 -04:00
Greg Johnston
be2d014f08 v0.5.1 2023-10-06 09:40:23 -04:00
Jesse He
18cdf70864 docs: fix hidden #two 2023-10-06 07:31:23 -04:00
Greg Johnston
1be25f0f47 fix: clippy "needless lifetimes" warning (closes #1825) (#1852) 2023-10-06 07:29:34 -04:00
Greg Johnston
cc93651bc9 fix: panic during generate_route_list if you immediately dispatch an action (#1853) 2023-10-06 07:29:10 -04:00
Kevin Old
a7a1559e01 fix: update log debug to use get_untracked for logged in user to resolve client side error (#1834) 2023-10-05 21:34:58 -04:00
martin frances
15f08aaa30 chore: removed warning in build artefacts. (#1840)
```
The following actions uses node12 which is deprecated and will be forced to run
   on node16: actions-rs/toolchain@v1. For more info:
   https://github.blog/changelog/2023-06-13-github-actions-all-actions-will-run-on-node16-instead-of-node12-by-default/
```

In other places @3 was being used, so for consitency I have bumped everything up to @4

-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
2023-10-05 21:15:01 -04:00
blorbb
d0295bae01 feat: support stored values in with! and update! (#1836) 2023-10-05 21:14:33 -04:00
Ben Wishovich
5220c37edd fix: make Async Mode return Content-Type header in Response (#1851) 2023-10-05 21:13:33 -04:00
Paul Wagener
4f649e020c feat: allow disposing of Signal & StoredValue (#1849) 2023-10-05 21:02:50 -04:00
Greg Johnston
e0d15c1a09 fix: correctly quote spread attributes in {..attrs} syntax (closes #1826) (#1831) 2023-10-02 18:02:49 -04:00
Greg Johnston
6f9c40b0a8 fix: correctly handle Suspense with local resources during hydration (closes #1823) (#1824) 2023-10-02 18:02:35 -04:00
Daniel Santana
a946a0181d fix: don't overwrite <Html/> props (closes #1828) 2023-10-02 13:46:20 -04:00
Marc-Stefan Cassola
6d44540ab3 feat: optional fallbacks for Show, Suspense, and Transition (#1817) 2023-10-02 08:29:21 -04:00
Sebastian Dobe
6a547cb9db docs: DX improvements: add section about jetbrains intellij-rust (#1804) 2023-10-02 08:26:35 -04:00
Tanguy
ac8dd7af67 fix: template! cfg condition (#1822) 2023-10-02 07:52:54 -04:00
Greg Johnston
0962f699e4 Merge pull request #1818 from leptos-rs/1816
Missing docs and `Copy` impl for `Callback`
2023-10-01 08:27:29 -04:00
Greg Johnston
fcd1028fb7 docs: missing module-level docs for Callback 2023-09-30 15:52:47 -04:00
Greg Johnston
dc429e33ff fix: missing Copy impl on Callback 2023-09-30 15:52:11 -04:00
Greg Johnston
6b40ca36a5 docs: fix For view prop name (closes #1813) (#1814) 2023-09-30 15:49:26 -04:00
Greg Johnston
d869bc6675 chore: clippy 2023-09-29 20:36:15 -04:00
Greg Johnston
d8aeb82949 cargo fmt 2023-09-29 20:35:39 -04:00
Greg Johnston
32e8213ebf v0.5.0 2023-09-29 17:13:56 -04:00
Greg Johnston
fa2be59895 feat: better error handling for ScopedFuture (#1810) 2023-09-29 17:12:56 -04:00
Sirius902
321c522fa5 fix: extra set of brackets in generate_head_metadata (#1811) 2023-09-29 17:12:46 -04:00
Greg Johnston
7378b8581a fix: broken Suspense when a resource loads immediately (closes #1805) (#1809) 2023-09-29 14:44:49 -04:00
Ben Wishovich
2d634364a9 feat: set Content-Type header for all Responses to text/html;charset="utf-8" (#1803) 2023-09-29 13:51:15 -04:00
Greg Johnston
f7adf6f73d examples: fix hackernews examples oops 2023-09-29 13:36:13 -04:00
martin frances
fb914e1a50 chore: bump outdated dependencies in leptos_macro (#1796)
-attribute-derive = { version = "0.6", features = ["syn-full"] }
+attribute-derive = { version = "0.8", features = ["syn-full"] }
-itertools = "0.10"
+itertools = "0.11"
2023-09-29 13:05:57 -04:00
Julien Scholz
772bb1d60c fix: improve rust-analyzer auto-completion (#1782) 2023-09-29 13:05:13 -04:00
Antonin Peronnet
bd4d2202ea feat: standardize on a Callback type that is Copy (#1795) 2023-09-29 13:04:53 -04:00
Greg Johnston
870808e63f feat: implement From<Fn() -> T> for Signal<T> (#1801) 2023-09-29 09:13:27 -04:00
Ben Wishovich
d7fff5a8ab fix: render_route error message and matching of non standard routes (#1799) 2023-09-29 09:10:59 -04:00
jquesada2016
609afce544 feat: Scoped Futures (#1761) 2023-09-28 15:20:18 -04:00
messense
181bcadbe2 feat(leptos_config): only enable toml feature for the config dependency (#1788) 2023-09-27 19:42:25 -04:00
Greg Johnston
3f2a9facf8 change: enable inline children for For by switching to children and bind: (#1773) 2023-09-26 14:24:02 -04:00
Saeed Andalib
c5c79234f1 docs: update working_with_signals.md (#1785)
Pulled the option number 2 out of the blockquote
2023-09-26 14:23:47 -04:00
Fangdun Tsai
de9fb5e382 chore(leptos_meta): enhance links in docs (#1783) 2023-09-25 20:34:11 -04:00
Greg Johnston
c9d132f007 change: use let: instead of bind: (#1774) 2023-09-25 20:33:36 -04:00
Greg Johnston
a1a9d41a7a updating SSR benchmarks to include tachys 2023-09-25 15:45:52 -04:00
Greg Johnston
73112c9faa Merge pull request #1779 from leptos-rs/docs-show
docs: fix `Show` docs reference to scope
2023-09-25 07:54:38 -04:00
Sean Aye
50678dafe1 feat: add JS Fetch integration support (#1554) 2023-09-25 07:51:25 -04:00
Greg Johnston
b1363a16ab docs: fix Show docs reference to scope 2023-09-23 12:46:41 -04:00
Greg Johnston
ae986e71fa change: only run create_local_resource in the browser (#1777) 2023-09-23 11:10:50 -04:00
Antonin Peronnet
0531831fe8 chore: add cargo-make and trunk in nix flake (#1763)
* add `cargo-make` dependency for nix
* add `trunk` dependency for nix
2023-09-23 11:10:24 -04:00
Greg Johnston
18eeee8e1f 0.5.0-rc3 2023-09-22 17:38:54 -04:00
Greg Johnston
d99269afac docs: error in view! macro if you use cx, (#1772) 2023-09-22 17:29:55 -04:00
Nico Burniske
38d1727e9c change: generate_route_list no longer async in any integration (#1485) 2023-09-22 15:42:58 -04:00
Greg Johnston
e0265252d7 fix: broken benchmarks (closes #1763) (#1771) 2023-09-22 15:41:44 -04:00
Fangdun Tsai
6cc92cee8d chore(leptos_hot_reload): apply lints suggestions (#1735) 2023-09-22 13:48:23 -04:00
Fangdun Tsai
1d392483b4 chore(leptos_marco): enhancement of document generation (#1768) 2023-09-22 13:32:58 -04:00
Village
3b864ac1a0 feat: Static Site Generation (#1649) 2023-09-22 13:32:09 -04:00
Danik Vitek
baa5ea83fa fix: reimplement Oco cloning (#1749) 2023-09-22 13:31:04 -04:00
Gabriel de Perthuis
d651400fa2 docs: better document the interaction of SsrModes with blocking resources (#1765)
Meant to address users making the same mistake as
https://github.com/leptos-rs/leptos/issues/1119
2023-09-22 12:58:28 -04:00
Fangdun Tsai
b729a658df chore(leptos_router): improve docs (#1769) 2023-09-22 12:56:49 -04:00
Gabriel de Perthuis
2c8f46466b feat: support default values for annotated server_fn arguments with #[server(default)] (#1762)
This allows form submission with checkbox inputs to work.
For example:

    let doit = create_server_action::<DoItSFn>();
    <ActionForm action=doit>
      <input type="checkbox" name="is_good" value="true"/>
      <input type="submit"/>
    </ActionForm>

    #[server(DoItSFn, "/api")]
    pub async fn doit(#[server(default)] is_good: bool) -> Result<(), ServerFnError> {}

If is_good is absent in the request to the server API,
`Default::default()` is used instead.
2023-09-20 20:43:20 -04:00
Greg Johnston
f2117b1186 fix: restore missing run_as_child 2023-09-20 19:37:39 -04:00
Greg Johnston
726cf47f17 Merge pull request #1758 from leptos-rs/sus2
Fix Suspense issues on subsequent navigations
2023-09-20 13:32:35 -04:00
Greg Johnston
1759a3e149 feat: correctly use_context between islands (#1747) 2023-09-19 21:16:47 -04:00
Fangdun Tsai
2374439cd8 chore(server_fn): improve docs in server_fn (#1734) 2023-09-19 21:16:30 -04:00
Greg Johnston
f85bfd31db fix: Transition double-rendering 2023-09-19 21:04:41 -04:00
Greg Johnston
43b58bfba9 Revert "fix: #1742 part 2 (Suspense running children a second time => extra animations)"
This reverts commit fafb6c01da.
2023-09-19 21:04:02 -04:00
Greg Johnston
fafb6c01da fix: #1742 part 2 (Suspense running children a second time => extra animations) 2023-09-19 10:32:25 -04:00
Greg Johnston
1bd47f34e5 feat: make Transition set_pending use #[prop(into)] (#1746) 2023-09-18 22:46:03 -04:00
Greg Johnston
661a038780 fix: Resource::with() (pt. 3!) — closes #1751 without breaking #1742 or #1711 (#1752) 2023-09-18 22:45:50 -04:00
Fangdun Tsai
e706a69139 chore(server_fn_macro): improve docs (#1733) 2023-09-18 20:48:47 -04:00
Greg Johnston
2b59ae18bc fix: Resource::with() pt. 2 — (closes #1742 without reopening #1711) (#1750) 2023-09-18 16:13:48 -04:00
Lukas Potthast
7d3e2a41b9 fix: Callback clone impls missing a generic (#1744) 2023-09-18 13:47:29 -04:00
Joseph Cruz
7ef57345ca fix(examples/error_boundary): ci error (#1739)
* fix(examples/build): maybe spawn client process

* docs(examples/error_boundary): add testing note
2023-09-17 20:38:03 -04:00
Greg Johnston
7e5169e66d 0.5.0-rc2 2023-09-15 20:06:56 -04:00
Greg Johnston
73a85b4955 feat: use attr: syntax rather than AdditionalAttributes (#1728) 2023-09-15 18:36:54 -04:00
Village
2c12256260 feat: allow component names to be paths (#1725) 2023-09-15 18:18:29 -04:00
Chris
a821abfb11 fix: relax bounds on LeptosRoutes (#1729) 2023-09-15 18:17:55 -04:00
Greg Johnston
20e5db22b8 fix: replace uses of create_effect internally with create_isomorphic_effect (closes #1709) (#1723) 2023-09-15 17:23:36 -04:00
Greg Johnston
54e8a536c4 fix: correctly register Resource::with() (closes #1711) (#1726) 2023-09-15 16:49:28 -04:00
Greg Johnston
afa67726c1 fix: document #[prop(default = ...)] as in Optional Props (closes #1710) (#1721) 2023-09-15 15:16:46 -04:00
Greg Johnston
1db3e9c686 feat: implement Serialize and Deserialize for Oco<_> (#1720) 2023-09-15 15:16:35 -04:00
blorbb
2fd6e0a2a8 feat: support move on with! macros (#1717) 2023-09-15 12:58:30 -04:00
Gabriel de Perthuis
af454c7643 docs: typo in table of contents (#1719) 2023-09-15 12:58:09 -04:00
Joseph Cruz
1a589fcf32 fix(examples/build): do not require stop to end trunk (#1713)
* fix(examples/build): let ctrl-c stop trunk

* doc(examples/build): add stop server hints
2023-09-14 17:07:16 -04:00
Joseph Cruz
af215d6ce8 fix: exclude markdown files from examples lists (#1716)
* fix(examples/gen-members): exclude markdown files

* fix(ci): exclude markdown files from examples list

* test(ci): simulate leptos change

* chore(ci) :remove simulated change
2023-09-14 16:58:46 -04:00
Greg Johnston
e9fef73f53 docs: note about 0.5 in book 2023-09-14 15:53:27 -04:00
Chris
7c9b118b2d docs: update out-of-date docs for component macro (#1696) 2023-09-14 13:47:04 -04:00
Cosmo Brain
5db2590bc6 feat: implement LeptosRoutes for &mut ServiceConfig in leptos_actix (#1706) 2023-09-13 20:56:03 -04:00
Lukas Potthast
dc1ba24470 fix: manual Clone and Debug impl for Callbacks (#1703) 2023-09-13 19:59:23 -04:00
Joseph Cruz
e384d53996 doc(examples): reference run instructions (#1705) 2023-09-13 19:57:50 -04:00
jquesada2016
946f9ff3e1 feat: impl From<HtmlElement<El>> for HtmlElement<AnyElement> (#1700) 2023-09-13 19:55:48 -04:00
Baptiste
8d690ac146 fix: IntoView impl for Rc<dyn Fn() -> impl IntoView> (#1698) 2023-09-13 19:55:08 -04:00
Greg Johnston
8245d77738 Merge pull request #1704 from leptos-rs/rc1-fixes
A few fixes to rc1
2023-09-13 16:51:19 -04:00
Greg Johnston
59c7684568 fix: warnings on hydrating () 2023-09-13 12:01:05 -04:00
Greg Johnston
a158e7f8bd fix: remove erroneous log 2023-09-13 12:00:56 -04:00
Joseph Cruz
c11c4b0e3e build(examples): make it easier to run examples (#1697)
* build(examples): support process management
* build(examples): manage trunk
* build(examples): manage cargo leptos
* doc(examples): add run instructions
2023-09-12 10:46:16 -04:00
Greg Johnston
fe42ac11a8 0.5.0-rc1 2023-09-11 21:08:14 -04:00
blorbb
00f8c9583d feat: with! macros (#1693) 2023-09-11 21:01:50 -04:00
Greg Johnston
a317874f93 change: run effects after a tick (#1680) 2023-09-11 21:01:35 -04:00
Greg Johnston
651356a9ec docs: add docs for #[island] macro (#1691) 2023-09-11 19:56:33 -04:00
Village
1c2327b2d6 feat: attr: and #[prop(attrs)] syntax for passing attributes down to components (#1628) 2023-09-10 15:19:53 -04:00
Michael Jarvis
8c3e0f23b0 docs: fix interlude_projecting_children.md (#1690) 2023-09-10 15:18:11 -04:00
martin frances
1719c0d352 chore: cleared "cargo doc" issue. (#1687)
warning: Rust code block is empty
   --> leptos_reactive/src/memo.rs:209:9
    |
209 |     /// ```
    |         ^^^
    |
    = note: `#[warn(rustdoc::invalid_rust_codeblocks)]` on by default
help: mark blocks that do not contain Rust code as text
    |
209 |     /// ```text
    |            ++++
2023-09-10 15:17:36 -04:00
Greg Johnston
bb78f64cd5 fix: broken mount_to_body in CSR mode (#1688) 2023-09-10 13:23:32 -04:00
Greg Johnston
2fe5be2483 fix: restore deleted extract_with_state function (#1683) 2023-09-10 07:55:13 -04:00
martin frances
929fe08525 chore: remove ambiguity surrounding serde version numbers. (#1685)
These lint warnings.

warning: /home/martin/build/leptos/leptos/Cargo.toml: dependency (serde) specified without providing a local path, Git repository, version, or workspace dependency to use. This will be considered an error in future versions
warning: /home/martin/build/leptos/leptos/Cargo.toml: dependency (serde_json) specified without providing a local path, Git repository, version, or workspace dependency to use. This will be considered an error in future versions
2023-09-09 16:15:34 -04:00
Greg Johnston
66dfef8729 Merge pull request #1681 from leptos-rs/docs 2023-09-08 17:11:47 -04:00
Greg Johnston
238d61ce1e feat: experimental islands (#1660) 2023-09-08 16:33:00 -04:00
Greg Johnston
2fa2bf1706 docs: format 2023-09-08 16:27:50 -04:00
Greg Johnston
a07984be9e docs: add runtime warnings for mixing view! and builder in SSR mode (closes #1645) 2023-09-08 16:27:29 -04:00
Greg Johnston
e8a7086546 docs: add section on small DX wins (closes #1310) 2023-09-08 16:12:27 -04:00
Greg Johnston
23d48d4c0e docs: remove stray references to Scope (closes #1671) 2023-09-08 16:02:45 -04:00
Greg Johnston
3342faa039 docs: discuss #[component(transparent)] in router docs (closes #1627) 2023-09-08 15:57:02 -04:00
Greg Johnston
6c24061c82 docs: emphasize that you should only render <Routes/> once (closes #1552, #1620) 2023-09-08 15:48:41 -04:00
Greg Johnston
b9a1fb7743 examples: add note about potential for memory leaks with nested signals (#1675) 2023-09-08 15:28:18 -04:00
martin frances
3c3fc969ac chore: removed resolver link warning in example (#1677) 2023-09-08 14:47:14 -04:00
blorbb
c87212f2d7 chore: remove (most) syn 1 dependencies (#1670) 2023-09-08 14:46:38 -04:00
Baptiste
b3a4c95dad feat: Rc-backed ChildrenFn (#1669) 2023-09-08 07:44:50 -04:00
Greg Johnston
de44b1f91f Merge pull request #1673 from martinfrances107/router_version_bump
Router version bump
2023-09-08 07:43:47 -04:00
Greg Johnston
689022661d change: move logging macros into a logging module to avoid name conflicts with log and tracing (#1658) 2023-09-08 07:42:58 -04:00
Joseph Cruz
905d46a09d refactor(examples): extract client process tasks (#1665) (#1666)
* doc(test-report): report trunk and node

* refactor(examples): extract client process tasks

* chore(exaples): force ci
2023-09-08 07:31:55 -04:00
martinfrances107
5585f20940 chore: Bumped a few outdated packages.
-cached = { version = "0.44.0", optional = true }
+cached = { version = "0.45.0", optional = true }
-lru = { version = "0.10", optional = true }
+lru = { version = "0.11", optional = true }
2023-09-08 09:30:13 +01:00
martinfrances107
5c3ed3f018 Chore: Bump to actions/checkout@v4 2023-09-08 08:28:01 +01:00
Greg Johnston
03cabf6ea3 chore: create SECURITY.md 2023-09-06 21:19:33 -04:00
SleeplessOne1917
2798dc455f examples: use cargo-leptos Tailwind support in Tailwind examples (#1625) 2023-09-06 07:25:00 -04:00
Florian Wickert
db20be5576 fix: compare path components to detect active link in router (#1656) 2023-09-06 06:49:10 -04:00
Nya
495862e9f9 fix: custom events on components (#1648) 2023-09-04 13:27:33 -04:00
Joseph Cruz
2ca1c51fdc test(error_boundary): add e2e testing (#1651)
* test(error_boundary): open app

* test(error_boundary): click up arrow

* test(error_boundary): click down arrow

* test(error_boundary): type number

* test(error_boundary): clear number

* fix(build): clean trunk directories

* fix(test-report): detect unit tests

* ci(build): echo stop trunk
2023-09-04 13:25:44 -04:00
Greg Johnston
70e1ad41e2 Merge pull request #1579 from leptos-rs/rusty
feat: start adding some Rustier interfaces for reactive types
2023-09-04 13:23:18 -04:00
Greg Johnston
53ec7ed272 feat: add Effect::with_value_mut() 2023-09-04 11:14:07 -04:00
Greg Johnston
d98a577740 feat: add Rustier interfaces for reactive system types 2023-09-04 11:05:23 -04:00
jquesada2016
fd834f48c2 change: rename .derived_signal() and .mapped_signal_setter() methods (#1637) 2023-09-04 08:41:41 -04:00
Greg Johnston
7be65a37c6 fix: versioned resources never decrement Suspense (closes #1640) (#1641) 2023-09-03 20:21:16 -04:00
Banzobotic
3b5e2d86fb docs: clean up messy spacing left over from cx replacements (#1626) 2023-09-03 20:21:05 -04:00
jquesada2016
716b9fb50b feat: add .into_X_boxed() for classes, properties, and styles as for attributes 2023-09-03 20:18:49 -04:00
jquesada2016
006ca13797 chore: hide get_property (#1638) 2023-09-03 20:15:15 -04:00
Village
6e008343c8 feat: add component generics (#1636) 2023-09-03 20:09:50 -04:00
Greg Johnston
2ca24883ac fix: memoize Suspense readiness to avoid rerendering children/fallback (#1642) 2023-09-03 20:07:20 -04:00
Village
4a43983f4e feat: implement spreading attributes onto elements (#1619) 2023-09-01 20:52:15 -04:00
IcosaHedron
d9e83121c1 feat: add reload websocket configuration and enable env configuration (#1613) 2023-09-01 20:51:46 -04:00
Antonin Peronnet
f5b4b97c9b feat: Callback types to make it easier to accept (optional) callback props (#1596) 2023-09-01 20:51:32 -04:00
Gareth
bcfa430a40 docs: fix incorrect variable name (#1623) 2023-09-01 07:39:41 -04:00
Lawrence Qupty
7c51815cf5 docs: remove extra space (#1622) 2023-09-01 07:39:05 -04:00
Dmitry Pytaylo
fee2fb953b docs: fix typo (#1618) 2023-09-01 07:37:52 -04:00
Sadra M
8ecb7f59c4 docs: update references to server binary in dockerfile (#1617) 2023-09-01 07:37:24 -04:00
martin frances
b85cb9fb3b docs: clarify how many times derived signals are called (#1614) 2023-09-01 07:36:15 -04:00
Joseph Cruz
a631c5ca1c doc(examples): report fantoccini use (#1616) 2023-09-01 07:35:29 -04:00
Greg Johnston
bee9bd8f67 0.5.0-beta2 2023-08-29 21:23:59 -04:00
Greg Johnston
8d3874f8a9 cargo fmt 2023-08-29 21:19:24 -04:00
Einherjar
bade16d227 docs: discuss unique paths for #[server] functions (#1610) 2023-08-29 20:49:31 -04:00
Jon Cahill
e0a132bde3 fix: don't try to parse as JSON the result from a server function redirect (#1604) 2023-08-29 20:42:19 -04:00
Daniel Oliveira
4d7e1f4d26 feat: improve server function client side error handling (#1597)
Handle all error codes 401-499 in addition to the
400 and 500-599 that were already handled.

In addition, handle them all in the same way
and improve the error message.
2023-08-29 20:40:03 -04:00
Maneren
700eee6604 fix(macro/params): clippy warning (#1612) 2023-08-29 20:31:54 -04:00
Joseph Cruz
694ed61e4c fix(ci): add new webkit dependency (#1607) (#1608) 2023-08-28 11:08:47 -04:00
Joseph Cruz
d7330097ba chore(examples): improve cucumber support #1598 (#1599)
* chore(examples): add cucumber runner

* chore(examples): clean cargo recursively
2023-08-28 11:08:22 -04:00
Greg Johnston
c65a3a6ca3 docs: add docs for builder syntax (#1603) 2023-08-28 11:08:07 -04:00
Danik Vitek
793c191619 feat: Oco (Owned Clones Once) smart pointer (#1480) 2023-08-26 11:43:51 -04:00
Greg Johnston
6c3e2fe53e feat: update to typed-builder 0.16 (closes #1455) (#1590) 2023-08-26 10:10:42 -04:00
Greg Johnston
08c419e3ee fix: broken test with untrack in tracing props (#1593) 2023-08-26 09:20:14 -04:00
Greg Johnston
736f4185b5 Merge pull request #1588 from leptos-rs/1457
Some resource and transition fixes
2023-08-26 07:34:21 -04:00
Greg Johnston
9cc0fc8c49 fix: adjust tracing properties 2023-08-26 07:24:52 -04:00
Greg Johnston
8f067dcde7 chore: clear release-mode warnings 2023-08-25 17:16:00 -04:00
Greg Johnston
ad6eb58fe1 fix: <Transition/> fallback in CSR 2023-08-25 17:12:01 -04:00
Greg Johnston
3f3ab1c3c8 remove unnecessary parens 2023-08-25 16:49:26 -04:00
Greg Johnston
9adae32847 examples: improve hackernews behavior 2023-08-25 16:00:47 -04:00
Greg Johnston
b8098e7992 fix: <Transition/> fallback on non-initial page loads 2023-08-25 16:00:47 -04:00
Greg Johnston
bef4d0dd3b fix: resource loading signal pattern for subsequent hydration page loads 2023-08-25 16:00:47 -04:00
Matt Cuneo
a789100e22 feat: allow autoreload websocket connection to work outside of localhost (#1548)
* Updated client reloading to use window.location.protocol/host to determine websocket connection. Added optional config reload_external_port to provide further control of the client websocket connection. These changes allow reloading while accessing the served site from outside of localhost.
2023-08-25 15:54:22 -04:00
Greg Johnston
abeca70625 fix: correct logic for resource loading signal when read outside suspense (#1586) 2023-08-25 11:46:54 -04:00
rkuklik
cc293b1170 feat: generic event handler types to make it easier to create collections of event handlers (#1444) 2023-08-25 11:41:16 -04:00
Greg Johnston
8ab62c17c6 feat: add Fn traits for resources on nightly (#1587) 2023-08-25 11:20:29 -04:00
Joseph Cruz
cf14e857ca refactor(check-stable): use matrix (#1543) (#1583)
* refactor(check-stable): use matrix

* chore: simulate leptos change

* chore: remove simulated change
2023-08-25 10:30:00 -04:00
Greg Johnston
c322ef38fd feat: signal traits should take associated types instead of generics (#1578) 2023-08-25 10:29:24 -04:00
Greg Johnston
c9cc493063 fix: fourth argument to server functions (#1585) 2023-08-25 10:28:54 -04:00
Joseph Cruz
fb48f7f117 fix(counters_stable): restore wasm tests (#1581) (#1582) 2023-08-24 16:33:01 -04:00
尹吉峰
c344e54cf6 feat: return an Effect from create_effect that can be disposed (#1571) 2023-08-24 10:24:10 -04:00
Greg Johnston
7306ecccbc feat: make struct name and path optional for server functions (#1573) 2023-08-24 10:22:35 -04:00
Greg Johnston
b98174db7a feat: support passing signals directly as attributes, classes, styles, and props on stable (#1577) 2023-08-24 10:22:14 -04:00
Greg Johnston
e48f66694d fix: runtime disposal time in render_to_string_async (#1574) 2023-08-24 10:22:00 -04:00
Mark Catley
533fccd1d3 fix: nightly warning in server macro for lifetime (#1580)
On the latest lifetime we're getting the following warning in the server
macro:
 warning: `&` without an explicit lifetime name cannot be used here
   --> src/login.rs:19:1
    |
 19 | #[server(Login, "/api")]
    | ^
    |
    = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
    = note: for more information, see issue #115010 <https://github.com/rust-lang/rust/issues/115010>
    = note: this warning originates in the attribute macro `server` (in Nightly builds, run with -Z macro-backtrace for more info)
2023-08-24 06:30:13 -04:00
Greg Johnston
ec4bd7600f fix: suppress warning about non-reactivity when calling .refetch() (occurs in e.g., async blocks in actions) (#1576)
* fix: suppress warning about non-reactivity when calling `.refetch()` (occurs in e.g., async blocks in actions)

* fix: don't reenter reactivity if these are nested
2023-08-23 20:57:05 -04:00
Greg Johnston
65d4e98d38 fix: INFO is too high a level for this prop tracing (#1570) 2023-08-23 06:39:37 -04:00
Nathan Lapel
195b843840 feat: remove Clone requirement for slots in vectors (#1564) 2023-08-22 21:23:38 -04:00
Joseph Cruz
00ac66e450 refactor(verify-changed-examples): improve readability and runtime (#1556)
* refactor(workflows): split setup

* test((verify-changed-examples): simulate  change

* refactor(verify-changes-examples): inline os setup

* refactor(verify-changed-examples): skip w/o change

* chore(verify-changed-examples): remove simulated change

* refactor(verify-changed-examples): revert inline

* refactor(verify-changes-examples): extract example changed

* fix(verify-changed-examples): pull up example changed

* refactor(verify-change-examples): extract matrix

* refactor(verify-changed-examples): pass input

* refactor(verify-changed-examples): rename workflow

* ci(workflows): install chromedriver if needed

* fix(ci-changed-examples): pass input from json

* perf(run-cargo-make-task): maybe install chromedriver

* fix((run-cargo-make-task): maybe install chromedriver

* perf(run-cargo-make-task): maybe install playwrigh deps

* fix(run-cargo-make-task): maybe install playwrigh deps

* chore(suspsense_tests): retry e2e

* refactor(verify-changed-examples): rename calls

* refactor(run-cargo-make-test): remove playwright count
2023-08-22 21:22:13 -04:00
Joseph Cruz
351701036b refactor(workflows): extract calls (#1566)
* refactor(workflows): extract leptos changed

* refactor(workflows): rename matrix job

* refactor(workflows): extract examples matrix

* chore(workflows): simulate leptos change

* chore(workflows): remove simulated leptos change
2023-08-22 21:19:58 -04:00
Greg Johnston
2bead5dadd docs: note 0.4 vs 0.5 in README.md 2023-08-21 19:35:23 -04:00
Greg Johnston
dbc707adcd feat/change: adopt reactive ownership model and drop cx/Scope (#918) 2023-08-21 19:31:37 -04:00
Greg Johnston
5066242ef3 remove file accidentally included from islands branch 2023-08-20 19:52:47 -04:00
Greg Johnston
e9deff52a7 v0.4.9 2023-08-20 14:27:49 -04:00
Greg Johnston
eb3d9b8714 build(docs): only publish on new version tags (#1562) 2023-08-20 09:38:35 -04:00
luoxiaozero
18deb398ca feat: tracing support for component props (#1531) 2023-08-18 08:29:41 -04:00
Geert Stappers
d9abebb4be docs: add link to source code for book 2023-08-18 07:57:38 -04:00
Jonathan
a480db8b77 <Show/> update (#1557) 2023-08-18 07:50:26 -04:00
Greg Johnston
1f26b68d45 docs: inner_html in book 2023-08-16 21:31:06 -04:00
Greg Johnston
937501c61b docs: add note about #[component(transparent)] 2023-08-16 21:26:53 -04:00
Joseph Cruz
5523fb86fb perf(check-stable): only run on source change (#1542) 2023-08-15 06:20:01 -04:00
Joseph Cruz
7dcfcf8ca8 chore(test_examples): remove obsolete directory (#1540) 2023-08-15 06:19:36 -04:00
Joseph Cruz
087c68569a test(suspense-tests): add e2e tests (Closes #1519) (#1533)
* test(suspense-tests): add e2e tests (closes #1519)

test(suspense_tests): load nested

test(suspense_tests): load parallel

test(suspsense_tests): load nested inside

test(suspense_tests): load single

test(suspense_tests): load inside component

test(suspense_tests): load no resources

test(suspense_tests): click nested count

test(suspense_tests): click inside component count

test(suspense_tests): click nested inside count

test(suspense_tests): click single count

test(suspense_tests): click parallel counts

test(suspense_tests): click no resources count

refactor(suspense_tests): change view strategy

* fix(suspense_tests): let_unit_value
2023-08-15 06:19:20 -04:00
Milo Moisson
6abfdd2345 examples: on_cleanup misorder? in fetch examples (#1532)
* Update api.rs

* fix: second hackernews example
2023-08-15 06:18:38 -04:00
martin frances
cddd784e8d chore: fixed lint warning seen while running ``cargo doc`` (#1539)
"component" is both a module and a macro and so we must
disambiguate
2023-08-15 06:18:19 -04:00
Greg Johnston
f6978217fb docs: give a compile error when trying to put a child inside a self-closing HTML tag (closes #1535) (#1537) 2023-08-13 12:44:45 -04:00
Greg Johnston
aa58cedc15 Merge pull request #1529 from leptos-rs/docs-advanced-reactivity
Add advanced docs on reactive graph, and update testing docs
2023-08-11 13:52:08 -04:00
Greg Johnston
a0b0d72d19 docs: update testing section 2023-08-11 13:51:10 -04:00
Greg Johnston
fa8d0945e0 docs: add section on reactive graph internals 2023-08-11 13:36:27 -04:00
Greg Johnston
3ed49381e3 docs: expand on the need for prop:value (#1526) 2023-08-10 14:59:12 -04:00
Greg Johnston
8ec3fb95f0 docs: typos in NavigateOptions docs (#1525) 2023-08-09 20:44:39 -04:00
Greg Johnston
cc11430d16 docs: add use_navigate to router docs in guide (#1524) 2023-08-09 20:44:31 -04:00
Greg Johnston
0b650ee2dc Merge pull request #1523 from leptos-rs/more-docs
Additional random docs
2023-08-09 20:24:48 -04:00
Greg Johnston
4def35cb45 docs: add <Await/> 2023-08-09 20:24:04 -04:00
Greg Johnston
0e56f27e0d docs: add watch 2023-08-09 20:19:12 -04:00
Greg Johnston
bd8983f462 docs: expand docs on Axum State/FromRef pattern 2023-08-09 20:14:37 -04:00
Greg Johnston
7ef635d9cf docs: deployment 2023-08-09 20:09:54 -04:00
Joseph Cruz
19ea6fae6a test(todo_app_sqlite_axum): add e2e tests (#1514) (#1515)
* refactor(examples): pull up cargo leptos tasks

* test(todo_app_sqlite_axum): add e2e tests
2023-08-09 08:37:28 -04:00
Joseph Cruz
651a111db9 fix(suspense-tests): build errors (#1517) (#1518) 2023-08-09 08:36:25 -04:00
Danik Vitek
3a98bdb3c2 fix: use current pathname for create_query_signal (#1508) 2023-08-07 20:25:22 -04:00
Greg Johnston
f01b982cff fix: render empty dynamic text node in HTML as (closes #1382) (#1507) 2023-08-07 18:04:56 -04:00
Joseph Cruz
69dd96f76f test(todo_app_sqlite): add e2e tests (#1448) (#1467) 2023-08-07 17:51:24 -04:00
starmaker
329ae08e60 chore: enable stable support for rkyv feature (#1503) 2023-08-07 08:54:02 -04:00
Greg Johnston
1e13ad8fee perf: in hydration, reuse existing text node rather than destroying and remounting (#1506) 2023-08-07 08:34:10 -04:00
Geert Stappers
e0c9a9523a docs: typo
Signed-off-by: Geert Stappers <stappers@stappers.nl>
2023-08-04 10:56:51 -04:00
Mark Catley
0726a3034d examples: fix github links (#1493) 2023-08-04 07:55:04 -04:00
Greg Johnston
a88d047eff template refactor + snapshot tests (#1435) 2023-08-04 07:54:03 -04:00
mateusvmv
4001561987 fix: scoping of JS variable names in inline scripts (#1489) 2023-08-03 08:46:06 -04:00
Greg Johnston
2f860b37bd v0.4.8 2023-08-02 19:25:32 -04:00
Greg Johnston
b86009b9d0 fix: remove erroneous logging 2023-08-02 19:16:32 -04:00
Greg Johnston
54733e1b34 v0.4.7 2023-08-02 17:03:38 -04:00
Greg Johnston
56f01888b7 Merge pull request #1486 from leptos-rs/export-all-helpers
fix: correctly export all DOM helpers
2023-08-02 17:02:19 -04:00
Greg Johnston
8320f16716 chore: fix new clippy warnings 2023-08-02 16:05:42 -04:00
Greg Johnston
0b16e5992d fix: correctly export all DOM helpers 2023-08-02 14:41:54 -04:00
Danik Vitek
248beb4a55 docs: typo in docs for ServerFnErrorErr (#1477) 2023-08-01 14:27:39 -04:00
martin frances
c9f608d030 docs: fix doclink to Error (#1469) 2023-08-01 13:24:13 -04:00
Greg Johnston
f837d3e6a2 fix: correctly escape HTML in DynChild text nodes (closes #1475) (#1478) 2023-08-01 13:22:24 -04:00
Greg Johnston
8847d5fc42 fix: compile-time regression for deeply-nested component trees (#1476) 2023-07-31 14:23:09 -04:00
Greg Johnston
7819a6fac0 fix: properly replace text nodes in DynChild (closes #1456) (#1472) 2023-07-30 22:37:53 -04:00
Marco Inacio
c199185808 docs: README.md to reflect new version (#1470) 2023-07-30 11:52:09 -04:00
martin frances
e0b5738606 chore: document the magic number in FILTER_SHOW_COMMENT. (#1468) 2023-07-29 16:53:10 -04:00
Sebastian Dobe
f3e3880a57 fix: AnimatedShow - possible panic on cleanup (#1464) 2023-07-29 06:33:49 -04:00
Greg Johnston
d44b90c16d feat: allow mut in component props and suppress "needless lifetime" warning (closes #1458) (#1459) 2023-07-29 06:32:06 -04:00
Joseph Cruz
cc32a3e863 perf(examples): speed up the test-info report (#1446) (#1447) 2023-07-27 20:40:26 -04:00
Greg Johnston
5740c9b76b feat: add MaybeProp type (#1443) 2023-07-27 18:18:25 -04:00
Greg Johnston
80fa6ad3eb docs: fix typo in 23_ssr_modes.md (#1445) 2023-07-26 16:33:21 -04:00
Greg Johnston
7bc1ad2b4f fix: incorrect opening node for <Each/> in debug mode (closes #1168) (#1436) 2023-07-26 10:43:46 -04:00
Joseph Cruz
82a2fe7cbe fix(examples): unable to parse makefile (#1440) (#1441) 2023-07-26 10:43:20 -04:00
Bechma
40bf944957 docs: expand spawn_local documentation (#1433) 2023-07-25 11:42:48 -04:00
Greg Johnston
7ef7546fa9 v0.4.6 2023-07-25 06:08:53 -04:00
Greg Johnston
5e26e84d77 feat: allow feature-name flexibility when using server functions (#1427) 2023-07-25 06:07:52 -04:00
mforsb
e67bc2083a feat: add noscroll attribute to Form, ActionForm (#1432) 2023-07-25 06:07:37 -04:00
g-re-g
a3cb3f7f77 perf: use binary search for event and tag names in view macro (#1430) 2023-07-24 15:06:34 -04:00
Greg Johnston
daeb47e72e build(examples): update Makefiles for recent examples (#1431) 2023-07-24 12:02:30 -04:00
Joseph Cruz
8c5ab99fa7 build(examples): pull up compile tasks (#1417)
* build(examples): pull up compile tasks

* build(examples): set toolchain for compiles tasks

* build(examples): set toolchain for build and check

* build(examples): set toolchain of other examples
2023-07-24 11:35:34 -04:00
Greg Johnston
984a7388f1 fix: clear <title> correctly when navigating between pages (closes #1369) (#1428) 2023-07-24 11:25:28 -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
634 changed files with 36526 additions and 13977 deletions

View File

@@ -9,66 +9,21 @@ on:
- main
jobs:
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:
- name: Checkout
uses: actions/checkout@v3
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
- name: Install JQ Tool
uses: mbround18/install-jq@v1
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
- 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:
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:
test:
name: Check
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
needs: [get-leptos-changed, get-examples-matrix]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: nightly

View File

@@ -2,45 +2,25 @@ name: Check stable
on:
push:
branches: [main]
branches:
- main
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
branches:
- main
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
name: Check
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
matrix:
rust:
- stable
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 examples
run: cargo make --profile=github-actions check-stable
directory: [examples/counters_stable, examples/counter_without_macros]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: stable

View File

@@ -0,0 +1,32 @@
name: CI Changed Examples
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
get-example-changed:
uses: ./.github/workflows/get-example-changed.yml
get-matrix:
needs: [get-example-changed]
uses: ./.github/workflows/get-changed-examples-matrix.yml
with:
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
test:
name: CI
needs: [get-example-changed, get-matrix]
if: needs.get-example-changed.outputs.example_changed == 'true'
strategy:
matrix: ${{ fromJSON(needs.get-matrix.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -9,45 +9,13 @@ on:
- 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
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
- 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:
test:
name: CI
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
matrix:
directory:
@@ -73,3 +41,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -1,35 +1,29 @@
name: Verify Changed Examples
name: Changed Examples Matrix Call
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_call:
inputs:
example_changed:
description: "Example Changed"
required: true
type: boolean
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.get-example-changed.outputs.matrix }}
jobs:
setup:
name: Get Changes
get-example-changed:
name: Get Changed Example Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
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
@@ -40,8 +34,9 @@ jobs:
examples
!examples/cargo-make
!examples/gtk
!examples/hackernews_js_fetch
!examples/Makefile.toml
!examples/README.md
!examples/*.md
json: true
quotepath: false
@@ -51,21 +46,10 @@ jobs:
- name: Set Matrix
id: set-matrix
run: |
if [ ${{ steps.changed-files.outputs.any_changed }} == 'true' ]; then
if [ ${{ inputs.example_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"
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$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"

View File

@@ -0,0 +1,39 @@
name: Examples Changed Call
on:
workflow_call:
outputs:
example_changed:
description: "Example Changed"
value: ${{ jobs.get-example-changed.outputs.example_changed }}
jobs:
get-example-changed:
name: Get Example Changed
runs-on: ubuntu-latest
outputs:
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
with:
files: |
examples
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml
!examples/*.md
- name: List example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
- name: Set example_changed
id: set-example-changed
run: |
echo "example_changed=${{ steps.changed-files.outputs.any_changed }}" >> "$GITHUB_OUTPUT"

View File

@@ -0,0 +1,40 @@
name: Get Examples Matrix Call
on:
workflow_call:
outputs:
matrix:
description: "Matrix"
value: ${{ jobs.create.outputs.matrix }}
jobs:
create:
name: Create Examples Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4
- 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 .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: Print Location Info
run: |
echo "Workspace: ${{ github.workspace }}"
pwd
ls | sort -u

View File

@@ -0,0 +1,44 @@
name: Get Leptos Changed Call
on:
workflow_call:
outputs:
leptos_changed:
description: "Leptos Changed"
value: ${{ jobs.create.outputs.leptos_changed }}
jobs:
create:
name: Detect Source Change
runs-on: ubuntu-latest
outputs:
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
steps:
- name: Checkout
uses: actions/checkout@v4
- 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 leptos_changed
id: set-source-changed
run: |
echo "leptos_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"

View File

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

View File

@@ -9,34 +9,27 @@ on:
cargo_make_task:
required: true
type: string
toolchain:
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
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: 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
- uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
toolchain: ${{ inputs.toolchain }}
override: true
components: rustfmt
@@ -84,12 +77,38 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Maybe install chromedriver
run: |
project_makefile=${{inputs.directory}}/Makefile.toml
webdriver_count=$(cat $project_makefile | grep "cargo-make/webdriver.toml" | wc -l)
if [ $webdriver_count -eq 1 ]; then
if ! command -v chromedriver &>/dev/null; then
echo chromedriver required
sudo apt-get update
sudo apt-get install chromium-chromedriver
else
echo chromedriver is already installed
fi
else
echo chromedriver is not required
fi
- name: Maybe install playwright browser dependencies
run: |
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
if [ ! -v $pw_dir ]; then
echo "Playwright required in $pw_dir"
cd $pw_dir
pnpm dlx playwright install --with-deps
else
echo Playwright is not required
fi
done
# 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
cd ${{ inputs.directory }}
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}

View File

@@ -1,4 +1,4 @@
name: Verify All Examples
name: CI Examples
on:
workflow_dispatch:
@@ -10,38 +10,17 @@ on:
- 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
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
- 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]
test:
name: CI
needs: [get-examples-matrix]
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"
cargo_make_task: "ci"
toolchain: nightly

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ Cargo.lock
.idea
.direnv
.envrc
.vscode

View File

@@ -70,6 +70,25 @@ are a few guidelines that will make it a better experience for everyone:
`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

@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.4.5"
version = "0.5.2"
[workspace.dependencies]
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" }
leptos = { path = "./leptos", version = "0.5.2" }
leptos_dom = { path = "./leptos_dom", version = "0.5.2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.2" }
leptos_macro = { path = "./leptos_macro", version = "0.5.2" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.2" }
leptos_server = { path = "./leptos_server", version = "0.5.2" }
server_fn = { path = "./server_fn", version = "0.5.2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.2" }
leptos_config = { path = "./leptos_config", version = "0.5.2" }
leptos_router = { path = "./router", version = "0.5.2" }
leptos_meta = { path = "./meta", version = "0.5.2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.2" }
[profile.release]
codegen-units = 1

View File

@@ -20,6 +20,18 @@ cwd = "examples"
command = "cargo"
args = ["make", "ci-clean"]
[tasks.check-examples]
workspace = false
cwd = "examples"
command = "cargo"
args = ["make", "check-clean"]
[tasks.build-examples]
workspace = false
cwd = "examples"
command = "cargo"
args = ["make", "build-clean"]
[tasks.clean-examples]
workspace = false
cwd = "examples"

View File

@@ -10,15 +10,17 @@
[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)
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
# Leptos
```rust
use leptos::*;
#[component]
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
// create a reactive signal with the initial value
let (value, set_value) = create_signal(cx, initial_value);
let (value, set_value) = create_signal(initial_value);
// create event handlers for our buttons
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
@@ -27,23 +29,29 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
let increment = move |_| set_value.update(|value| *value += 1);
// create user interfaces with the declarative `view!` macro
view! { cx,
view! {
<div>
<button on:click=clear>"Clear"</button>
<button on:click=decrement>"-1"</button>
<button on:click=clear>Clear</button>
<button on:click=decrement>-1</button>
// text nodes can be quoted or unquoted
<span>"Value: " {value} "!"</span>
<button on:click=increment>"+1"</button>
<button on:click=increment>+1</button>
</div>
}
}
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
pub fn main() {
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 /> })
mount_to_body(|| view! {
<SimpleCounter initial_value=3 />
})
}
```
### Important Note
This example, and the entire `main` branch, now reflect the upcoming `0.5.1` release. You can use `0.5.1` with the `0.5.1-beta` release on crates.io or by a git dependency on the `main` branch of this repo. [Click here for the 0.4.9 `README`](https://crates.io/crates/leptos).
## About the Framework
Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces.
@@ -88,8 +96,6 @@ targets = ["wasm32-unknown-unknown"]
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
> Note: The `nightly` feature is present on the main branch version right now, but not in 0.3.x. For 0.3.x, nightly is the default and `stable` has a special feature.
## `cargo-leptos`
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
@@ -115,7 +121,7 @@ People usually mean one of three things by this question.
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
The APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
The APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases, in terms of architecture.
2. **Are there bugs?**
@@ -154,13 +160,13 @@ There are some practical differences that make a significant difference:
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
```rust
let (count, set_count) = create_signal(cx, 0); // a signal
let (count, set_count) = create_signal(0); // a signal
let double_count = move || count() * 2; // a derived signal
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
let memoized_count = create_memo(move |_| count() * 3); // a memo
// all are accessed by calling them
assert_eq!(count(), 0);
assert_eq!(double_count(), 0);

13
SECURITY.md Normal file
View File

@@ -0,0 +1,13 @@
# Security Policy
## Reporting a Vulnerability
To report a suspected security issue, please contact security@leptos.dev rather than opening
a public issue.
## Supported Versions
The most-recently-released version of the library is supported with security updates.
For example, if a security issue is discovered that affects 0.3.2 and all later releases,
a 0.4.x patch will be released but a new 0.3.x patch release will not be made. You should
plan to update to the latest version to receive any new features or bugfixes of any kind.

View File

@@ -4,10 +4,16 @@ version = "0.1.0"
edition = "2021"
[dependencies]
l021 = { package = "leptos", version = "0.2.1" }
leptos = { path = "../leptos", features = ["ssr"] }
l0410 = { package = "leptos", version = "0.4.10", features = [
"nightly",
"ssr",
] }
leptos = { path = "../leptos", features = ["ssr", "nightly"] }
leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] }
tachydom = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
yew = { version = "0.20", features = ["ssr"] }
tokio-test = "0.4"
miniserde = "0.1"
gloo = "0.8"
@@ -20,7 +26,6 @@ strum_macros = "0.24"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
tera = "1"
reactive-signals = "0.1.0-alpha.4"
[dependencies.web-sys]
version = "0.3"

View File

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

View File

@@ -7,19 +7,16 @@ fn leptos_deep_creation(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
let signal = create_rw_signal(0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
}
})
.dispose()
}
});
runtime.dispose();
@@ -31,20 +28,17 @@ fn leptos_deep_update(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let signal = create_rw_signal(cx, 0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(cx, move |_| prev.get() + 1));
} else {
memos.push(create_memo(cx, move |_| signal.get() + 1));
}
let signal = create_rw_signal(0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
})
.dispose()
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
});
runtime.dispose();
@@ -56,17 +50,12 @@ fn leptos_narrowing_down(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
})
.dispose()
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo =
create_memo(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
assert_eq!(memo(), 499500);
});
runtime.dispose();
@@ -78,16 +67,13 @@ fn leptos_fanning_out(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let sig = create_rw_signal(cx, 0);
let memos = (0..1000)
.map(|_| create_memo(cx, move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
})
.dispose()
let sig = create_rw_signal(0);
let memos = (0..1000)
.map(|_| create_memo(move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
});
runtime.dispose();
@@ -98,143 +84,36 @@ fn leptos_narrowing_update(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, |cx| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
}
});
assert_eq!(acc.get(), 499500);
writes[1].update(|n| *n += 1);
writes[10].update(|n| *n += 1);
writes[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo(), 499503);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope(runtime, {
let acc = Rc::clone(&acc);
move |cx| {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r());
}
});
w.update(|n| *n += 1);
}
})
})
.collect::<Vec<_>>();
for disposer in disposers {
disposer.dispose();
}
});
runtime.dispose();
}
#[bench]
fn rs_deep_update(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let sc = ClientRuntime::new_root_scope();
b.iter(|| {
let signal = signal!(sc, 0);
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(signal!(sc, move || prev.get() + 1))
} else {
memos.push(signal!(sc, move || signal.get() + 1))
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
});
}
#[bench]
fn rs_fanning_out(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let sig = signal!(cx, 0);
let memos = (0..1000)
.map(|_| signal!(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
});
}
#[bench]
fn rs_narrowing_update(b: &mut Bencher) {
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let sigs =
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let memo = signal!(cx, {
let sigs = sigs.clone();
move || {
sigs.iter().map(|r| r.get()).sum::<i32>()
}
});
assert_eq!(memo.get(), 499500);
signal!(cx, {
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo =
create_memo(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
assert_eq!(memo(), 499500);
create_isomorphic_effect({
let acc = Rc::clone(&acc);
move || {
acc.set(memo.get());
move |_| {
acc.set(memo());
}
});
assert_eq!(acc.get(), 499500);
sigs[1].update(|n| *n += 1);
sigs[10].update(|n| *n += 1);
sigs[100].update(|n| *n += 1);
writes[1].update(|n| *n += 1);
writes[10].update(|n| *n += 1);
writes[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo.get(), 499503);
assert_eq!(memo(), 499503);
});
runtime.dispose();
}
#[bench]
fn l021_deep_creation(b: &mut Bencher) {
use l021::*;
fn l0410_deep_creation(b: &mut Bencher) {
use l0410::*;
let runtime = create_runtime();
b.iter(|| {
@@ -256,8 +135,8 @@ fn l021_deep_creation(b: &mut Bencher) {
}
#[bench]
fn l021_deep_update(b: &mut Bencher) {
use l021::*;
fn l0410_deep_update(b: &mut Bencher) {
use l0410::*;
let runtime = create_runtime();
b.iter(|| {
@@ -281,8 +160,8 @@ fn l021_deep_update(b: &mut Bencher) {
}
#[bench]
fn l021_narrowing_down(b: &mut Bencher) {
use l021::*;
fn l0410_narrowing_down(b: &mut Bencher) {
use l0410::*;
let runtime = create_runtime();
b.iter(|| {
@@ -304,8 +183,8 @@ fn l021_narrowing_down(b: &mut Bencher) {
}
#[bench]
fn l021_fanning_out(b: &mut Bencher) {
use leptos::*;
fn l0410_fanning_out(b: &mut Bencher) {
use l0410::*;
let runtime = create_runtime();
b.iter(|| {
@@ -324,8 +203,8 @@ fn l021_fanning_out(b: &mut Bencher) {
runtime.dispose();
}
#[bench]
fn l021_narrowing_update(b: &mut Bencher) {
use l021::*;
fn l0410_narrowing_update(b: &mut Bencher) {
use l0410::*;
let runtime = create_runtime();
b.iter(|| {
@@ -338,11 +217,11 @@ fn l021_narrowing_update(b: &mut Bencher) {
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
assert_eq!(memo.get(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
acc.set(memo.get());
}
});
assert_eq!(acc.get(), 499500);
@@ -352,7 +231,7 @@ fn l021_narrowing_update(b: &mut Bencher) {
writes[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo(), 499503);
assert_eq!(memo.get(), 499503);
})
.dispose()
});
@@ -361,8 +240,8 @@ fn l021_narrowing_update(b: &mut Bencher) {
}
#[bench]
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
use l021::*;
fn l0410_scope_creation_and_disposal(b: &mut Bencher) {
use l0410::*;
let runtime = create_runtime();
b.iter(|| {
@@ -375,7 +254,7 @@ fn l021_scope_creation_and_disposal(b: &mut Bencher) {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r());
acc.set(r.get());
}
});
w.update(|n| *n += 1);

View File

@@ -2,15 +2,14 @@ use test::Bencher;
#[bench]
fn leptos_ssr_bench(b: &mut Bencher) {
use leptos::*;
let r = create_runtime();
b.iter(|| {
use leptos::*;
leptos_dom::HydrationCtx::reset_id();
_ = create_scope(create_runtime(), |cx| {
leptos::leptos_dom::HydrationCtx::reset_id();
#[component]
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial);
fn Counter(initial: i32) -> impl IntoView {
let (value, set_value) = create_signal(initial);
view! {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
@@ -20,7 +19,6 @@ fn leptos_ssr_bench(b: &mut Bencher) {
}
let rendered = view! {
cx,
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
@@ -28,14 +26,51 @@ fn leptos_ssr_bench(b: &mut Bencher) {
<Counter initial=2/>
<Counter initial=3/>
</main>
}.into_view(cx).render_to_string(cx);
}.into_view().render_to_string();
assert_eq!(
rendered,
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here&#x27;s some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>"
"<main data-hk=\"0-0-1\"><h1 data-hk=\"0-0-2\">Welcome to our benchmark page.</h1><p data-hk=\"0-0-3\">Here&#x27;s some introductory text.</p><div data-hk=\"0-0-5\"><button data-hk=\"0-0-6\">-1</button><span data-hk=\"0-0-7\">Value: <!>1<!--hk=0-0-8-->!</span><button data-hk=\"0-0-9\">+1</button></div><!--hk=0-0-4--><div data-hk=\"0-0-11\"><button data-hk=\"0-0-12\">-1</button><span data-hk=\"0-0-13\">Value: <!>2<!--hk=0-0-14-->!</span><button data-hk=\"0-0-15\">+1</button></div><!--hk=0-0-10--><div data-hk=\"0-0-17\"><button data-hk=\"0-0-18\">-1</button><span data-hk=\"0-0-19\">Value: <!>3<!--hk=0-0-20-->!</span><button data-hk=\"0-0-21\">+1</button></div><!--hk=0-0-16--></main>"
);
});
});
r.dispose();
}
#[bench]
fn tachys_ssr_bench(b: &mut Bencher) {
use leptos::{create_runtime, create_signal, SignalGet, SignalUpdate};
use tachy_maccy::view;
use tachydom::view::Render;
let rt = create_runtime();
b.iter(|| {
fn counter(initial: i32) -> impl Render {
let (value, set_value) = create_signal(initial);
view! {
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
}
}
let mut buf = String::with_capacity(1024);
let rendered = view! {
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
{counter(1)}
{counter(2)}
{counter(3)}
</main>
};
rendered.to_html(&mut buf, &Default::default());
assert_eq!(
buf,
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>2<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>3<!>!</span><button>+1</button></div></main>"
);
});
rt.dispose();
}
#[bench]

View File

@@ -1,7 +1,7 @@
pub use leptos::*;
use miniserde::*;
use web_sys::HtmlInputElement;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Todos(pub Vec<Todo>);
@@ -9,13 +9,13 @@ pub struct Todos(pub Vec<Todo>);
const STORAGE_KEY: &str = "todos-leptos";
impl Todos {
pub fn new(cx: Scope) -> Self {
pub fn new() -> Self {
Self(vec![])
}
pub fn new_with_1000(cx: Scope) -> Self {
pub fn new_with_1000() -> Self {
let todos = (0..1000)
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
.map(|id| Todo::new(id, format!("Todo #{id}")))
.collect();
Self(todos)
}
@@ -72,13 +72,17 @@ pub struct Todo {
}
impl Todo {
pub fn new(cx: Scope, id: usize, title: String) -> Self {
Self::new_with_completed(cx, id, title, false)
pub fn new(id: usize, title: String) -> Self {
Self::new_with_completed(id, title, false)
}
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
let (title, set_title) = create_signal(cx, title);
let (completed, set_completed) = create_signal(cx, completed);
pub fn new_with_completed(
id: usize,
title: String,
completed: bool,
) -> Self {
let (title, set_title) = create_signal(title);
let (completed, set_completed) = create_signal(completed);
Self {
id,
title,
@@ -98,7 +102,7 @@ const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
pub fn TodoMVC(todos: Todos) -> impl IntoView {
let mut next_id = todos
.0
.iter()
@@ -107,10 +111,10 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
.map(|last| last + 1)
.unwrap_or(0);
let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos);
let (todos, set_todos) = create_signal(todos);
provide_context(set_todos);
let (mode, set_mode) = create_signal(cx, Mode::All);
let (mode, set_mode) = create_signal(Mode::All);
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
@@ -120,7 +124,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(cx, next_id, title.to_string());
let new = Todo::new(next_id, title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
@@ -128,7 +132,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
}
};
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
@@ -148,7 +152,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
create_effect(cx, move |_| {
create_effect(move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
.get()
@@ -163,7 +167,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
}
});
view! { cx,
view! {
<main>
<section class="todoapp">
<header class="header">
@@ -188,8 +192,8 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
<For
each=filtered_todos
key=|todo| todo.id
view=move |cx, todo: Todo| {
view! { cx, <Todo todo=todo.clone()/> }
children=move |todo: Todo| {
view! { <Todo todo=todo.clone()/> }
}
/>
</ul>
@@ -236,14 +240,14 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}.into_view(cx)
}.into_view()
}
#[component]
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
//let input = NodeRef::new(cx);
pub fn Todo(todo: Todo) -> impl IntoView {
let (editing, set_editing) = create_signal(false);
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
//let input = NodeRef::new();
let save = move |value: &str| {
let value = value.trim();
@@ -255,7 +259,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
set_editing(false);
};
view! { cx,
view! {
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
<div class="view">
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
@@ -268,7 +272,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
{move || {
editing()
.then(|| {
view! { cx,
view! {
<input
class="edit"
class:hidden=move || !(editing)()
@@ -319,8 +323,8 @@ pub struct TodoSerialized {
}
impl TodoSerialized {
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
pub fn into_todo(self, ) -> Todo {
Todo::new_with_completed(self.id, self.title, self.completed)
}
}

View File

@@ -2,6 +2,7 @@ use test::Bencher;
mod leptos;
mod sycamore;
mod tachys;
mod tera;
mod yew;
@@ -12,18 +13,37 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
b.iter(|| {
use crate::todomvc::leptos::*;
let html = ::leptos::ssr::render_to_string(|cx| {
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
let html = ::leptos::ssr::render_to_string(|| {
view! { <TodoMVC todos=Todos::new()/> }
});
assert!(html.len() > 1);
});
runtime.dispose();
}
#[bench]
fn tachys_todomvc_ssr(b: &mut Bencher) {
use ::leptos::*;
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::tachys::*;
use tachydom::view::Render;
let mut buf = String::new();
let rendered = TodoMVC(Todos::new());
rendered.to_html(&mut buf, &Default::default());
assert_eq!(
buf,
"<main><section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input placeholder=\"What needs to be done?\" autofocus=\"\" class=\"new-todo\"></header><section class=\"main hidden\"><input id=\"toggle-all\" type=\"checkbox\" class=\"toggle-all\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer hidden\"><span class=\"todo-count\"><strong>0</strong><!> items<!> left</span><ul class=\"filters\"><li><a href=\"#/\" class=\"selected selected\">All</a></li><li><a href=\"#/active\" class=\"\">Active</a></li><li><a href=\"#/completed\" class=\"\">Completed</a></li></ul><button class=\"clear-completed hidden hidden\">Clear completed</button></footer></section><footer class=\"info\"><p>Double-click to edit a todo</p><p>Created by <a href=\"http://todomvc.com\">Greg Johnston</a></p><p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p></footer></main>"
);
});
runtime.dispose();
}
#[bench]
fn sycamore_todomvc_ssr(b: &mut Bencher) {
use self::sycamore::*;
use ::sycamore::prelude::*;
use ::sycamore::*;
use ::sycamore::{prelude::*, *};
b.iter(|| {
_ = create_scope(|cx| {
@@ -42,8 +62,7 @@ fn sycamore_todomvc_ssr(b: &mut Bencher) {
#[bench]
fn yew_todomvc_ssr(b: &mut Bencher) {
use self::yew::*;
use ::yew::prelude::*;
use ::yew::ServerRenderer;
use ::yew::{prelude::*, ServerRenderer};
b.iter(|| {
tokio_test::block_on(async {
@@ -60,21 +79,35 @@ fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::leptos::*;
use ::leptos::*;
let html = ::leptos::ssr::render_to_string(|cx| {
let html = ::leptos::ssr::render_to_string(|| {
view! {
cx,
<TodoMVC todos=Todos::new_with_1000(cx)/>
<TodoMVC todos=Todos::new_with_1000()/>
}
});
assert!(html.len() > 1);
});
}
#[bench]
fn tachys_todomvc_ssr_with_1000(b: &mut Bencher) {
use ::leptos::*;
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::tachys::*;
use tachydom::view::Render;
let mut buf = String::new();
let rendered = TodoMVC(Todos::new_with_1000());
rendered.to_html(&mut buf, &Default::default());
assert!(buf.len() > 20_000)
});
runtime.dispose();
}
#[bench]
fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::sycamore::*;
use ::sycamore::prelude::*;
use ::sycamore::*;
use ::sycamore::{prelude::*, *};
b.iter(|| {
_ = create_scope(|cx| {
@@ -93,8 +126,7 @@ fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
#[bench]
fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::yew::*;
use ::yew::prelude::*;
use ::yew::ServerRenderer;
use ::yew::{prelude::*, ServerRenderer};
b.iter(|| {
tokio_test::block_on(async {
@@ -103,4 +135,19 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
assert!(rendered.len() > 1);
});
});
}
}
#[bench]
fn tera_todomvc_ssr(b: &mut Bencher) {
use ::leptos::*;
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::leptos::*;
let html = ::leptos::ssr::render_to_string(|| {
view! { <TodoMVC todos=Todos::new()/> }
});
assert!(html.len() > 1);
});
runtime.dispose();
}

View File

@@ -0,0 +1,324 @@
pub use leptos_reactive::*;
use miniserde::*;
use tachy_maccy::view;
use tachydom::view::Render;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Todos(pub Vec<Todo>);
const STORAGE_KEY: &str = "todos-leptos";
impl Todos {
pub fn new() -> Self {
Self(vec![])
}
pub fn new_with_1000() -> Self {
let todos = (0..1000)
.map(|id| Todo::new(id, format!("Todo #{id}")))
.collect();
Self(todos)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn add(&mut self, todo: Todo) {
self.0.push(todo);
}
pub fn remove(&mut self, id: usize) {
self.0.retain(|todo| todo.id != id);
}
pub fn remaining(&self) -> usize {
self.0.iter().filter(|todo| !(todo.completed)()).count()
}
pub fn completed(&self) -> usize {
self.0.iter().filter(|todo| (todo.completed)()).count()
}
pub fn toggle_all(&self) {
// if all are complete, mark them all active instead
if self.remaining() == 0 {
for todo in &self.0 {
if todo.completed.get() {
(todo.set_completed)(false);
}
}
}
// otherwise, mark them all complete
else {
for todo in &self.0 {
(todo.set_completed)(true);
}
}
}
fn clear_completed(&mut self) {
self.0.retain(|todo| !todo.completed.get());
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Todo {
pub id: usize,
pub title: ReadSignal<String>,
pub set_title: WriteSignal<String>,
pub completed: ReadSignal<bool>,
pub set_completed: WriteSignal<bool>,
}
impl Todo {
pub fn new(id: usize, title: String) -> Self {
Self::new_with_completed(id, title, false)
}
pub fn new_with_completed(
id: usize,
title: String,
completed: bool,
) -> Self {
let (title, set_title) = create_signal(title);
let (completed, set_completed) = create_signal(completed);
Self {
id,
title,
set_title,
completed,
set_completed,
}
}
pub fn toggle(&self) {
self.set_completed
.update(|completed| *completed = !*completed);
}
}
const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
pub fn TodoMVC(todos: Todos) -> impl Render {
let mut next_id = todos
.0
.iter()
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
let (todos, set_todos) = create_signal(todos);
provide_context(set_todos);
let (mode, set_mode) = create_signal(Mode::All);
let add_todo = move |ev: web_sys::KeyboardEvent| {
todo!()
/* let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(next_id, title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
}
} */
};
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
.0
.iter()
.filter(|todo| !todo.completed.get())
.cloned()
.collect(),
Mode::Completed => todos
.0
.iter()
.filter(|todo| todo.completed.get())
.cloned()
.collect(),
})
});
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
create_effect(move |_| {
()
/* if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
.get()
.0
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
let json = json::to_string(&objs);
if storage.set_item(STORAGE_KEY, &json).is_err() {
log::error!("error while trying to set item in localStorage");
}
} */
});
view! {
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus=""
/>
</header>
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
<input
id="toggle-all"
class="toggle-all"
r#type="checkbox"
//prop:checked=move || todos.with(|t| t.remaining() > 0)
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label r#for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
{filtered_todos.get().into_iter().map(Todo).collect::<Vec<_>>()}
</ul>
</section>
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
" left"
</span>
<ul class="filters">
<li>
<a
href="#/"
class="selected"
class:selected=move || mode() == Mode::All
>
"All"
</a>
</li>
<li>
<a href="#/active" class:selected=move || mode() == Mode::Active>
"Active"
</a>
</li>
<li>
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
"Completed"
</a>
</li>
</ul>
<button
class="clear-completed hidden"
class:hidden=move || todos.with(|t| t.completed() == 0)
on:click=move |_| set_todos.update(|t| t.clear_completed())
>
"Clear completed"
</button>
</footer>
</section>
<footer class="info">
<p>"Double-click to edit a todo"</p>
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}
}
pub fn Todo(todo: Todo) -> impl Render {
let (editing, set_editing) = create_signal(false);
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
//let input = NodeRef::new();
let save = move |value: &str| {
let value = value.trim();
if value.is_empty() {
set_todos.update(|t| t.remove(todo.id));
} else {
(todo.set_title)(value.to_string());
}
set_editing(false);
};
view! {
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
/* <div class="view">
<input class="toggle" r#type="checkbox"/>
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
<button
class="destroy"
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
></button>
</div>
{move || {
editing()
.then(|| {
view! {
<input
class="edit"
class:hidden=move || !(editing)()
/>
}
})
}} */
</li>
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Active,
Completed,
All,
}
impl Default for Mode {
fn default() -> Self {
Mode::All
}
}
pub fn route(hash: &str) -> Mode {
match hash {
"/active" => Mode::Active,
"/completed" => Mode::Completed,
_ => Mode::All,
}
}
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
pub id: usize,
pub title: String,
pub completed: bool,
}
impl TodoSerialized {
pub fn into_todo(self) -> Todo {
Todo::new_with_completed(self.id, self.title, self.completed)
}
}
impl From<&Todo> for TodoSerialized {
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: (todo.completed)(),
}
}
}

View File

@@ -87,7 +87,7 @@ static TEMPLATE: &str = r#"<main>
</main>"#;
#[bench]
fn tera_todomvc(b: &mut Bencher) {
fn tera_todomvc_ssr(b: &mut Bencher) {
use serde::{Deserialize, Serialize};
use tera::*;
@@ -127,7 +127,7 @@ fn tera_todomvc(b: &mut Bencher) {
}
#[bench]
fn tera_todomvc_1000(b: &mut Bencher) {
fn tera_todomvc_ssr_1000(b: &mut Bencher) {
use serde::{Deserialize, Serialize};
use tera::*;
@@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) {
let _ = TERA.render("template.html", &ctx).unwrap();
});
}
}

View File

@@ -9,10 +9,10 @@ This document is intended as a running list of common issues, with example code
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
```rust
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, false);
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(false);
create_effect(cx, move |_| {
create_effect(move |_| {
if a() > 5 {
set_b(true);
}
@@ -24,7 +24,7 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
```rust
let (a, set_a) = create_signal(cx, 0);
let (a, set_a) = create_signal(0);
let b = move || a () > 5;
```
@@ -34,19 +34,19 @@ Sometimes you have nested signals: for example, hash-map that can change over ti
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let resources = create_rw_signal(cx, HashMap::new());
pub fn App() -> impl IntoView {
let resources = create_rw_signal(HashMap::new());
let update = move |id: usize| {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(cx, 0))
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
};
view! { cx,
view! {
<div>
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
<button on:click=move |_| update(1)>"+"</button>
@@ -55,17 +55,17 @@ pub fn App(cx: Scope) -> impl IntoView {
}
```
Clicking the button twice will cause a panic, because of the nested signal *read*. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
You can fix this fairly easily by using the [`Scope::batch()`](https://docs.rs/leptos/latest/leptos/struct.Scope.html#method.batch) method:
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
```rust
let update = move |id: usize| {
cx.batch(move || {
batch(move || {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(cx, 0))
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
});
@@ -83,11 +83,11 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
```rust
let (a, set_a) = create_signal(cx, "Starting value".to_string());
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
view! {
cx,
// ❌ reactivity doesn't work as expected: typing only updates the default
// of each input, so if you start typing in the second input, it won't
// update the first one
@@ -97,11 +97,11 @@ view! {
```
```rust
let (a, set_a) = create_signal(cx, "Starting value".to_string());
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
view! {
cx,
// ✅ works as intended by setting the value *property*
<input prop:value=a on:input=on_input />
<input prop:value=a on:input=on_input />

View File

@@ -1,2 +1,10 @@
[output.html]
additional-css = ["./mdbook-admonish.css"]
[output.html.playground]
runnable = false
[preprocessor]
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`

View File

@@ -0,0 +1,353 @@
@charset "UTF-8";
:root {
--md-admonition-icon--note:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
--md-admonition-icon--abstract:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
--md-admonition-icon--info:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
--md-admonition-icon--tip:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
--md-admonition-icon--success:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
--md-admonition-icon--question:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
--md-admonition-icon--warning:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
--md-admonition-icon--failure:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
--md-admonition-icon--danger:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
--md-admonition-icon--bug:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
--md-admonition-icon--example:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
--md-admonition-icon--quote:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
--md-details-icon:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
}
:is(.admonition) {
display: flow-root;
margin: 1.5625em 0;
padding: 0 1.2rem;
color: var(--fg);
page-break-inside: avoid;
background-color: var(--bg);
border: 0 solid black;
border-inline-start-width: 0.4rem;
border-radius: 0.2rem;
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.05), 0 0 0.1rem rgba(0, 0, 0, 0.1);
}
@media print {
:is(.admonition) {
box-shadow: none;
}
}
:is(.admonition) > * {
box-sizing: border-box;
}
:is(.admonition) :is(.admonition) {
margin-top: 1em;
margin-bottom: 1em;
}
:is(.admonition) > .tabbed-set:only-child {
margin-top: 0;
}
html :is(.admonition) > :last-child {
margin-bottom: 1.2rem;
}
a.admonition-anchor-link {
display: none;
position: absolute;
left: -1.2rem;
padding-right: 1rem;
}
a.admonition-anchor-link:link, a.admonition-anchor-link:visited {
color: var(--fg);
}
a.admonition-anchor-link:link:hover, a.admonition-anchor-link:visited:hover {
text-decoration: none;
}
a.admonition-anchor-link::before {
content: "§";
}
:is(.admonition-title, summary.admonition-title) {
position: relative;
min-height: 4rem;
margin-block: 0;
margin-inline: -1.6rem -1.2rem;
padding-block: 0.8rem;
padding-inline: 4.4rem 1.2rem;
font-weight: 700;
background-color: rgba(68, 138, 255, 0.1);
display: flex;
}
:is(.admonition-title, summary.admonition-title) p {
margin: 0;
}
html :is(.admonition-title, summary.admonition-title):last-child {
margin-bottom: 0;
}
:is(.admonition-title, summary.admonition-title)::before {
position: absolute;
top: 0.625em;
inset-inline-start: 1.6rem;
width: 2rem;
height: 2rem;
background-color: #448aff;
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
-webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-size: contain;
content: "";
}
:is(.admonition-title, summary.admonition-title):hover a.admonition-anchor-link {
display: initial;
}
details.admonition > summary.admonition-title::after {
position: absolute;
top: 0.625em;
inset-inline-end: 1.6rem;
height: 2rem;
width: 2rem;
background-color: currentcolor;
mask-image: var(--md-details-icon);
-webkit-mask-image: var(--md-details-icon);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-size: contain;
content: "";
transform: rotate(0deg);
transition: transform 0.25s;
}
details[open].admonition > summary.admonition-title::after {
transform: rotate(90deg);
}
:is(.admonition):is(.note) {
border-color: #448aff;
}
:is(.note) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(68, 138, 255, 0.1);
}
:is(.note) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #448aff;
mask-image: var(--md-admonition-icon--note);
-webkit-mask-image: var(--md-admonition-icon--note);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.abstract, .summary, .tldr) {
border-color: #00b0ff;
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 176, 255, 0.1);
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00b0ff;
mask-image: var(--md-admonition-icon--abstract);
-webkit-mask-image: var(--md-admonition-icon--abstract);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.info, .todo) {
border-color: #00b8d4;
}
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 184, 212, 0.1);
}
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00b8d4;
mask-image: var(--md-admonition-icon--info);
-webkit-mask-image: var(--md-admonition-icon--info);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.tip, .hint, .important) {
border-color: #00bfa5;
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 191, 165, 0.1);
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00bfa5;
mask-image: var(--md-admonition-icon--tip);
-webkit-mask-image: var(--md-admonition-icon--tip);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.success, .check, .done) {
border-color: #00c853;
}
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 200, 83, 0.1);
}
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00c853;
mask-image: var(--md-admonition-icon--success);
-webkit-mask-image: var(--md-admonition-icon--success);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.question, .help, .faq) {
border-color: #64dd17;
}
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(100, 221, 23, 0.1);
}
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #64dd17;
mask-image: var(--md-admonition-icon--question);
-webkit-mask-image: var(--md-admonition-icon--question);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.warning, .caution, .attention) {
border-color: #ff9100;
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 145, 0, 0.1);
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff9100;
mask-image: var(--md-admonition-icon--warning);
-webkit-mask-image: var(--md-admonition-icon--warning);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.failure, .fail, .missing) {
border-color: #ff5252;
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 82, 82, 0.1);
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff5252;
mask-image: var(--md-admonition-icon--failure);
-webkit-mask-image: var(--md-admonition-icon--failure);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.danger, .error) {
border-color: #ff1744;
}
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 23, 68, 0.1);
}
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff1744;
mask-image: var(--md-admonition-icon--danger);
-webkit-mask-image: var(--md-admonition-icon--danger);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.bug) {
border-color: #f50057;
}
:is(.bug) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(245, 0, 87, 0.1);
}
:is(.bug) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #f50057;
mask-image: var(--md-admonition-icon--bug);
-webkit-mask-image: var(--md-admonition-icon--bug);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.example) {
border-color: #7c4dff;
}
:is(.example) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(124, 77, 255, 0.1);
}
:is(.example) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #7c4dff;
mask-image: var(--md-admonition-icon--example);
-webkit-mask-image: var(--md-admonition-icon--example);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.quote, .cite) {
border-color: #9e9e9e;
}
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(158, 158, 158, 0.1);
}
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #9e9e9e;
mask-image: var(--md-admonition-icon--quote);
-webkit-mask-image: var(--md-admonition-icon--quote);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
.navy :is(.admonition) {
background-color: var(--sidebar-bg);
}
.ayu :is(.admonition), .coal :is(.admonition) {
background-color: var(--theme-hover);
}
.rust :is(.admonition) {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.rust .admonition-anchor-link:link, .rust .admonition-anchor-link:visited {
color: var(--sidebar-fg);
}

View File

@@ -1,20 +1,22 @@
# Introduction
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
It will walk through the fundamental concepts you need to build applications,
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
It will walk through the fundamental concepts you need to build applications,
beginning with a simple application rendered in the browser, and building toward a
full-stack application with server-side rendering and hydration.
The guide doesnt assume you know anything about fine-grained reactivity or the
details of modern Web frameworks. It does assume you are familiar with the Rust
The guide doesnt assume you know anything about fine-grained reactivity or the
details of modern Web frameworks. It does assume you are familiar with the Rust
programming language, HTML, CSS, and the DOM and basic Web APIs.
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
understand Leptos.
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
**The guide is a work in progress.**
**Important Note**: This current version of the book reflects the `0.5.1` release. The CodeSandbox versions of the examples still reflect `0.4` and earlier APIs and are in the process of being updated.
> The source code for the book is available [here](https://github.com/leptos-rs/leptos/tree/main/docs/book). PRs for typos or clarification are always welcome.

View File

@@ -26,20 +26,33 @@ cargo init leptos-tutorial
cargo add leptos --features=csr,nightly
```
Or you can leave off `nighly` if you're using stable Rust
> **Note**: This version of the book reflects the Leptos 0.5 release. The CodeSandbox examples have not yet been updated from 0.4 and earlier versions.
Or you can leave off `nightly` 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
> To use nightly Rust, you can either opt into nightly for all your Rust projects by running
>
> ```bash
> rustup toolchain install nightly
> rustup default nightly
> ```
>
> or only for this project
>
> ```bash
> rustup toolchain install nightly
> cd <into your project>
> rustup override set nightly
> ```
>
> [See here for more details.](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html)
>
> If youd rather use stable Rust with Leptos, you can do that too. In the guide and examples, youll just use the [`ReadSignal::get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) and [`WriteSignal::set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) methods instead of calling signal getters and setters as functions.
Make sure you've added the `wasm32-unknown-unknown` target so that Rust can compile your code to WebAssembly to run in the browser.
@@ -64,7 +77,7 @@ And add a simple “Hello, world!” to your `main.rs`
use leptos::*;
fn main() {
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
mount_to_body(|| view! { <p>"Hello, world!"</p> })
}
```

View File

@@ -29,15 +29,15 @@ all its children and descendants using `provide_context`.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> 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);
let (count, set_count) = create_signal(0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(cx, count);
provide_context(count);
view! { cx,
view! {
// SetterButton is allowed to modify the count
<SetterButton set_count/>
// These consumers can only read from it
@@ -57,14 +57,14 @@ fn App(cx: Scope) -> impl IntoView {
```rust
/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath(cx: Scope) -> impl IntoView {
fn FancyMath() -> impl IntoView {
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>(cx)
let count = use_context::<ReadSignal<u32>>()
// 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,
view! {
<div class="consumer blue">
"The number "
<strong>{count}</strong>
@@ -89,17 +89,17 @@ struct GlobalState {
}
impl GlobalState {
pub fn new(cx: Scope) -> Self {
pub fn new() -> Self {
Self {
count: create_rw_signal(cx, 0),
name: create_rw_signal(cx, "Bob".to_string())
count: create_rw_signal(0),
name: create_rw_signal("Bob".to_string())
}
}
}
#[component]
fn App(cx: Scope) -> impl IntoView {
provide_context(cx, GlobalState::new(cx));
fn App() -> impl IntoView {
provide_context(GlobalState::new());
// etc.
}
@@ -117,8 +117,8 @@ struct GlobalState {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
provide_context(cx, create_rw_signal(GlobalState::default()));
fn App() -> impl IntoView {
provide_context(create_rw_signal(GlobalState::default()));
// etc.
}
@@ -127,8 +127,8 @@ fn App(cx: Scope) -> impl IntoView {
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,
let state = expect_context::<RwSignal<GlobalState>>();
view! {
<button on:click=move |_| state.update(|n| *n += 1)>"+1"</button>
<p>{move || state.with(|state| state.name.clone())}</p>
}
@@ -143,12 +143,12 @@ Here, instead of reading from the state signal directly, we create “slices”
```rust
/// A component that updates the count in the global state.
#[component]
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
let state = expect_context::<RwSignal<GlobalState>>(cx);
fn GlobalStateCounter() -> impl IntoView {
let state = expect_context::<RwSignal<GlobalState>>();
// `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
@@ -157,7 +157,7 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|state, n| state.count = n,
);
view! { cx,
view! {
<div class="consumer blue">
<button
on:click=move |_| {
@@ -180,9 +180,9 @@ 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)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/15-global-state-0-5-8c2ff6?file=%2Fsrc%2Fmain.rs%3A1%2C2)
<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>
<iframe src="https://codesandbox.io/p/sandbox/15-global-state-0-5-8c2ff6?file=%2Fsrc%2Fmain.rs%3A1%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -214,15 +214,15 @@ use leptos::*;
// 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 {
fn Option2() -> 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);
let (count, set_count) = create_signal(0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(cx, count);
provide_context(count);
view! { cx,
view! {
<h1>"Option 2: Passing Signals"</h1>
// SetterButton is allowed to modify the count
<SetterButton set_count/>
@@ -237,8 +237,8 @@ fn Option2(cx: Scope) -> impl IntoView {
/// A button that increments our global counter.
#[component]
fn SetterButton(cx: Scope, set_count: WriteSignal<u32>) -> impl IntoView {
view! { cx,
fn SetterButton(set_count: WriteSignal<u32>) -> impl IntoView {
view! {
<div class="provider red">
<button on:click=move |_| set_count.update(|count| *count += 1)>
"Increment Global Count"
@@ -249,14 +249,14 @@ fn SetterButton(cx: Scope, set_count: WriteSignal<u32>) -> impl IntoView {
/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath(cx: Scope) -> impl IntoView {
fn FancyMath() -> impl IntoView {
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>(cx)
let count = use_context::<ReadSignal<u32>>()
// 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,
view! {
<div class="consumer blue">
"The number "
<strong>{count}</strong>
@@ -272,17 +272,17 @@ fn FancyMath(cx: Scope) -> impl IntoView {
/// A component that shows a list of items generated from the global count.
#[component]
fn ListItems(cx: Scope) -> impl IntoView {
fn ListItems() -> 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 count = use_context::<ReadSignal<u32>>().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> })
.map(|n| view! { <li>{n}<sup>"2"</sup> " is " {n * n}</li> })
.collect::<Vec<_>>()
};
view! { cx,
view! {
<div class="consumer green">
<ul>{squares}</ul>
</div>
@@ -304,13 +304,13 @@ struct GlobalState {
}
#[component]
fn Option3(cx: Scope) -> impl IntoView {
fn Option3() -> 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);
let state = create_rw_signal(GlobalState::default());
provide_context(state);
view! { cx,
view! {
<h1>"Option 3: Passing Signals"</h1>
<div class="red consumer" style="width: 100%">
<h2>"Current Global State"</h2>
@@ -329,12 +329,12 @@ fn Option3(cx: Scope) -> impl IntoView {
/// 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");
fn GlobalStateCounter() -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>().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
@@ -343,7 +343,7 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|state, n| state.count = n,
);
view! { cx,
view! {
<div class="consumer blue">
<button
on:click=move |_| {
@@ -360,14 +360,13 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
/// 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");
fn GlobalStateInput() -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>().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
@@ -376,7 +375,7 @@ fn GlobalStateInput(cx: Scope) -> impl IntoView {
|state, n| state.name = n,
);
view! { cx,
view! {
<div class="consumer green">
<input
type="text"
@@ -395,9 +394,8 @@ fn GlobalStateInput(cx: Scope) -> impl IntoView {
// 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/> })
leptos::mount_to_body(|| view! { <Option2/><Option3/> })
}
```
</details>

View File

@@ -7,11 +7,13 @@
- [Dynamic Attributes](./view/02_dynamic_attributes.md)
- [Components and Props](./view/03_components.md)
- [Iteration](./view/04_iteration.md)
- [Iterating over More Complex Data](./view/04b_iteration.md)
- [Forms and Inputs](./view/05_forms.md)
- [Control Flow](./view/06_control_flow.md)
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.md)
- [No Macros: The View Builder Syntax](./view/builder.md)
- [Reactivity](./reactivity/README.md)
- [Working with Signals](./reactivity/working_with_signals.md)
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
@@ -43,5 +45,8 @@
- [Responses and Redirects](./server/27_response.md)
- [Progressive Enhancement and Graceful Degradation](./progressive_enhancement/README.md)
- [`<ActionForm/>`s](./progressive_enhancement/action_form.md)
- [Deployment]()
- [Deployment](./deployment.md)
- [Guide: Islands](./islands.md)
- [Appendix: How Does the Reactive System Work?](./appendix_reactive_graph.md)
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)
- [Appendix: Some Small DX Improvements](./appendix_dx.md)

View File

@@ -0,0 +1,60 @@
# A Running List of Small Developer Experience Improvements
## Autocompletion inside `#[component]` and `#[server]`
Because of the nature of macros (they can expand from anything to anything, but only if the input is exactly correct at that instant) it can be hard for rust-analyzer to do proper autocompletion and other support.
But you can tell rust-analyzer to ignore certain proc macros. For `#[component]` and `#[server]` especially, which annotate function bodies but don't actually transform anything inside the body of your function, this can be really helpful.
Note that this means that rust-analyzer doesn't know about your component props, which may generate its own set of errors or warnings in the IDE.
VSCode `settings.json`:
```json
"rust-analyzer.procMacro.ignored": {
"leptos_macro": [
"server",
"component"
],
}
```
neovim with lspconfig:
```lua
require('lspconfig').rust_analyzer.setup {
-- Other Configs ...
settings = {
["rust-analyzer"] = {
-- Other Settings ...
procMacro = {
ignored = {
leptos_macro = {
"server",
"component",
},
},
},
},
}
}
```
Helix, in `.helix/languages.toml`:
```toml
[[language]]
name = "rust"
config = { procMacro = {ignored = {leptos_macro = ["component"]}}}
```
```admonish info
The Jetbrains `intellij-rust` plugin (RustRover as well) currently does not support dynamic config for macro exclusion.
However, the project currently maintains a hardcoded list of excluded macros.
As soon as [this open PR](https://github.com/intellij-rust/intellij-rust/pull/10873) is merged, the `component` and
`server` macro will be excluded automatically without additional configuration needed.
Update (2023/10/02):
The `intellij-rust` plugin got deprecated in favor of RustRover at the same time the PR was opened, but an official
support request was made to integrate the contents of this PR.
```

View File

@@ -0,0 +1,243 @@
# Appendix: How does the Reactive System Work?
You dont need to know very much about how the reactive system actually works in order to use the library successfully. But its always useful to understand whats going on behind the scenes once you start working with the framework at an advanced level.
The reactive primitives you use are divided into three sets:
- **Signals** (`ReadSignal`/`WriteSignal`, `RwSignal`, `Resource`, `Trigger`) Values you can actively change to trigger reactive updates.
- **Computations** (`Memo`s) Values that depend on signals (or other computations) and derive a new reactive value through some pure computation.
- **Effects** Observers that listen to changes in some signals or computations and run a function, causing some side effect.
Derived signals are a kind of non-primitve computation: as plain closures, they simply allow you to refactor some repeated signal-based computation into a reusable function that can be called in multiple places, but they are not represented in the reactive system itself.
All the other primitives actually exist in the reactive system as nodes in a reactive graph.
Most of the work of the reactive system consists of propagating changes from signals to effects, possibly through some intervening memos.
The assumption of the reactive system is that effects (like rendering to the DOM or making a network request) are orders of magnitude more expensive than things like updating a Rust data structure inside your app.
So the **primary goal** of the reactive system is to **run effects as infrequently as possible**.
Leptos does this through the construction of a reactive graph.
> Leptoss current reactive system is based heavily on the [Reactively](https://github.com/modderme123/reactively) library for JavaScript. You can read Milos article “[Super-Charging Fine-Grained Reactivity](https://dev.to/modderme123/super-charging-fine-grained-reactive-performance-47ph)” for an excellent account of its algorithm, as well as fine-grained reactivity in general—including some beautiful diagrams!
## The Reactive Graph
Signals, memos, and effects all share three characteristics:
- **Value** They have a current value: either the signals value, or (for memos and effects) the value returned by the previous run, if any.
- **Sources** Any other reactive primitives they depend on. (For signals, this is an empty set.)
- **Subscribers** Any other reactive primitives that depend on them. (For effects, this is an empty set.)
In reality then, signals, memos, and effects are just conventional names for one generic concept of a “node” in a reactive graph. Signals are always “root nodes,” with no sources/parents. Effects are always “leaf nodes,” with no subscribers. Memos typically have both sources and subscribers.
### Simple Dependencies
So imagine the following code:
```rust
// A
let (name, set_name) = create_signal("Alice");
// B
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
// C
create_effect(move |_| {
log!("{}", name_upper());
});
set_name("Bob");
```
You can easily imagine the reactive graph here: `name` is the only signal/origin node, the `create_effect` is the only effect/terminal node, and theres one intervening memo.
```
A (name)
|
B (name_upper)
|
C (the effect)
```
### Splitting Branches
Lets make it a little more complex.
```rust
// A
let (name, set_name) = create_signal("Alice");
// B
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
// C
let name_len = create_memo(move |_| name.len());
// D
create_effect(move |_| {
log!("len = {}", name_len());
});
// E
create_effect(move |_| {
log!("name = {}", name_upper());
});
```
This is also pretty straightforward: a signal source signal (`name`/`A`) divides into two parallel tracks: `name_upper`/`B` and `name_len`/`C`, each of which has an effect that depends on it.
```
__A__
| |
B C
| |
D E
```
Now lets update the signal.
```rust
set_name("Bob");
```
We immediately log
```
len = 3
name = BOB
```
Lets do it again.
```rust
set_name("Tim");
```
The log should shows
```
name = TIM
```
`len = 3` does not log again.
Remember: the goal of the reactive system is to run effects as infrequently as possible. Changing `name` from `"Bob"` to `"Tim"` will cause each of the memos to re-run. But they will only notify their subscribers if their value has actually changed. `"BOB"` and `"TIM"` are different, so that effect runs again. But both names have the length `3`, so they do not run again.
### Reuniting Branches
One more example, of whats sometimes called **the diamond problem**.
```rust
// A
let (name, set_name) = create_signal("Alice");
// B
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
// C
let name_len = create_memo(move |_| name.len());
// D
create_effect(move |_| {
log!("{} is {} characters long", name_upper(), name_len());
});
```
What does the graph look like for this?
```
__A__
| |
B C
| |
|__D__|
```
You can see why it's called the “diamond problem.” If Id connected the nodes with straight lines instead of bad ASCII art, it would form a diamond: two memos, each of which depend on a signal, which feed into the same effect.
A naive, push-based reactive implementation would cause this effect to run twice, which would be bad. (Remember, our goal is to run effects as infrequently as we can.) For example, you could implement a reactive system such that signals and memos immediately propagate their changes all the way down the graph, through each dependency, essentially traversing the graph depth-first. In other words, updating `A` would notify `B`, which would notify `D`; then `A` would notify `C`, which would notify `D` again. This is both inefficient (`D` runs twice) and glitchy (`D` actually runs with the incorrect value for the second memo during its first run.)
## Solving the Diamond Problem
Any reactive implementation worth its salt is dedicated to solving this issue. There are a number of different approaches (again, [see Milos article](https://dev.to/modderme123/super-charging-fine-grained-reactive-performance-47ph) for an excellent overview).
Heres how ours works, in brief.
A reactive node is always in one of three states:
- `Clean`: it is known not to have changed
- `Check`: it is possible it has changed
- `Dirty`: it has definitely changed
Updating a signal `Dirty` marks that signal `Dirty`, and marks all its descendants `Check`, recursively. Any of its descendants that are effects are added to a queue to be re-run.
```
____A (DIRTY)___
| |
B (CHECK) C (CHECK)
| |
|____D (CHECK)__|
```
Now those effects are run. (All of the effects will be marked `Check` at this point.) Before re-running its computation, the effect checks its parents to see if they are dirty. So
- So `D` goes to `B` and checks if it is `Dirty`.
- But `B` is also marked `Check`. So `B` does the same thing:
- `B` goes to `A`, and finds that it is `Dirty`.
- This means `B` needs to re-run, because one of its sources has changed.
- `B` re-runs, generating a new value, and marks itself `Clean`
- Because `B` is a memo, it then checks its prior value against the new value.
- If they are the same, `B` returns "no change." Otherwise, it returns "yes, I changed."
- If `B` returned “yes, I changed,” `D` knows that it definitely needs to run and re-runs immediately before checking any other sources.
- If `B` returned “no, I didnt change,” `D` continues on to check `C` (see process above for `B`.)
- If neither `B` nor `C` has changed, the effect does not need to re-run.
- If either `B` or `C` did change, the effect now re-runs.
Because the effect is only marked `Check` once and only queued once, it only runs once.
If the naive version was a “push-based” reactive system, simply pushing reactive changes all the way down the graph and therefore running the effect twice, this version could be called “push-pull.” It pushes the `Check` status all the way down the graph, but then “pulls” its way back up. In fact, for large graphs it may end up bouncing back up and down and left and right on the graph as it tries to determine exactly which nodes need to re-run.
**Note this important trade-off**: Push-based reactivity propagates signal changes more quickly, at the expense of over-re-running memos and effects. Remember: the reactive system is designed to minimize how often you re-run effects, on the (accurate) assumption that side effects are orders of magnitude more expensive than this kind of cache-friendly graph traversal happening entirely inside the librarys Rust code. The measurement of a good reactive system is not how quickly it propagates changes, but how quickly it propagates changes _without over-notifying_.
## Memos vs. Signals
Note that signals always notify their children; i.e., a signal is always marked `Dirty` when it updates, even if its new value is the same as the old value. Otherwise, wed have to require `PartialEq` on signals, and this is actually quite an expensive check on some types. (For example, add an unnecessary equality check to something like `some_vec_signal.update(|n| n.pop())` when its clear that it has in fact changed.)
Memos, on the other hand, check whether they change before notifying their children. They only run their calculation once, no matter how many times you `.get()` the result, but they run whenever their signal sources change. This means that if the memos computation is _very_ expensive, you may actually want to memoize its inputs as well, so that the memo only re-calculates when it is sure its inputs have changed.
## Memos vs. Derived Signals
All of this is cool, and memos are pretty great. But most actual applications have reactive graphs that are quite shallow and quite wide: you might have 100 source signals and 500 effects, but no memos or, in rare case, three or four memos between the signal and the effect. Memos are extremely good at what they do: limiting how often they notify their subscribers that they have changed. But as this description of the reactive system should show, they come with overhead in two forms:
1. A `PartialEq` check, which may or may not be expensive.
2. Added memory cost of storing another node in the reactive system.
3. Added computational cost of reactive graph traversal.
In cases in which the computation itself is cheaper than this reactive work, you should avoid “over-wrapping” with memos and simply use derived signals. Heres a great example in which you should never use a memo:
```rust
let (a, set_a) = create_signal(1);
// none of these make sense as memos
let b = move || a() + 2;
let c = move || b() % 2 == 0;
let d = move || if c() { "even" } else { "odd" };
set_a(2);
set_a(3);
set_a(5);
```
Even though memoizing would technically save an extra calculation of `d` between setting `a` to `3` and `5`, these calculations are themselves cheaper than the reactive algorithm.
At the very most, you might consider memoizing the final node before running some expensive side effect:
```rust
let text = create_memo(move |_| {
d()
});
create_effect(move |_| {
engrave_text_into_bar_of_gold(&text());
});
```

View File

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

View File

@@ -3,14 +3,14 @@
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
```rust
let (count, set_count) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let (count, set_count) = create_signal(0);
let once = create_resource(count, |count| async move { load_a(count).await });
view! { cx,
view! {
<h1>"My Data"</h1>
{move || match once.read(cx) {
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
{move || match once.get() {
None => view! { <p>"Loading..."</p> }.into_view(),
Some(data) => view! { <ShowData data/> }.into_view()
}}
}
```
@@ -18,19 +18,19 @@ view! { cx,
But what if we have two resources, and want to wait for both of them?
```rust
let (count, set_count) = create_signal(cx, 0);
let (count2, set_count2) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
let (count, set_count) = create_signal(0);
let (count2, set_count2) = create_signal(0);
let a = create_resource(count, |count| async move { load_a(count).await });
let b = create_resource(count2, |count| async move { load_b(count).await });
view! { cx,
view! {
<h1>"My Data"</h1>
{move || match (a.read(cx), b.read(cx)) {
(Some(a), Some(b)) => view! { cx,
{move || match (a.get(), b.get()) {
(Some(a), Some(b)) => view! {
<ShowA a/>
<ShowA b/>
}.into_view(cx),
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
}.into_view(),
_ => view! { <p>"Loading..."</p> }.into_view()
}}
}
```
@@ -40,26 +40,26 @@ Thats not _so_ bad, but its kind of annoying. What if we could invert the
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If its still waiting for resources to load, it shows the `fallback`. When theyve all loaded, it shows the children.
```rust
let (count, set_count) = create_signal(cx, 0);
let (count2, set_count2) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
let (count, set_count) = create_signal(0);
let (count2, set_count2) = create_signal(0);
let a = create_resource(count, |count| async move { load_a(count).await });
let b = create_resource(count2, |count| async move { load_b(count).await });
view! { cx,
view! {
<h1>"My Data"</h1>
<Suspense
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
>
<h2>"My Data"</h2>
<h3>"A"</h3>
{move || {
a.read(cx)
.map(|a| view! { cx, <ShowA a/> })
a.get()
.map(|a| view! { <ShowA a/> })
}}
<h3>"B"</h3>
{move || {
b.read(cx)
.map(|b| view! { cx, <ShowB b/> })
b.get()
.map(|b| view! { <ShowB b/> })
}}
</Suspense>
}
@@ -69,9 +69,37 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
This inversion of the flow of control makes it easier to add or remove individual resources, as you dont need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which well talk about during a later chapter.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
## `<Await/>`
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
In youre simply trying to wait for some `Future` to resolve before rendering, you may find the `<Await/>` component helpful in reducing boilerplate. `<Await/>` essentially combines a resource with the source argument `|| ()` with a `<Suspense/>` with no fallback.
In other words:
1. It only polls the `Future` once, and does not respond to any reactive changes.
2. It does not render anything until the `Future` resolves.
3. After the `Future` resolves, its binds its data to whatever variable name you choose and then renders its children with that variable in scope.
```rust
async fn fetch_monkeys(monkey: i32) -> i32 {
// maybe this didn't need to be async
monkey * 2
}
view! {
<Await
// `future` provides the `Future` to be resolved
future=|| fetch_monkeys(3)
// the data is bound to whatever variable name you provide
let:data
>
// you receive the data by reference and can use it in your view here
<p>{*data} " little monkeys, jumping on the bed."</p>
</Await>
}
```
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-0-5-qzpgqs?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-0-5-qzpgqs?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -86,17 +114,17 @@ async fn important_api_call(name: String) -> String {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (name, set_name) = create_signal(cx, "Bill".to_string());
fn App() -> impl IntoView {
let (name, set_name) = create_signal("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,
view! {
<input
on:input=move |ev| {
set_name(event_target_value(&ev));
@@ -107,22 +135,21 @@ fn App(cx: Scope) -> impl IntoView {
<Suspense
// the fallback will show whenever a resource
// read "under" the suspense is loading
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <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)}
{move || async_data.get()}
</p>
</Suspense>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -6,9 +6,9 @@ Youll notice in the `<Suspense/>` example that if you keep reloading the data
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-0-5-2jg5lz?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<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>
<iframe src="https://codesandbox.io/p/sandbox/12-transition-0-5-2jg5lz?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -29,13 +29,13 @@ async fn important_api_call(id: usize) -> String {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (tab, set_tab) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (tab, set_tab) = create_signal(0);
// this will reload every time `tab` changes
let user_data = create_resource(cx, tab, |tab| async move { important_api_call(tab).await });
let user_data = create_resource(tab, |tab| async move { important_api_call(tab).await });
view! { cx,
view! {
<div class="buttons">
<button
on:click=move |_| set_tab(0)
@@ -65,19 +65,18 @@ fn App(cx: Scope) -> impl IntoView {
// the fallback will show initially
// on subsequent reloads, the current child will
// continue showing
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
>
<p>
{move || user_data.read(cx)}
{move || user_data.read()}
</p>
</Transition>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -16,22 +16,22 @@ async fn add_todo_request(new_title: &str) -> Uuid {
}
```
`create_action` takes a reactive `Scope` and an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
`create_action` takes an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
> The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
>
> ```rust
> // if there's a single argument, just use that
> let action1 = create_action(cx, |input: &String| {
> let action1 = create_action(|input: &String| {
> let input = input.clone();
> async move { todo!() }
> });
>
> // if there are no arguments, use the unit type `()`
> let action2 = create_action(cx, |input: &()| async { todo!() });
> let action2 = create_action(|input: &()| async { todo!() });
>
> // if there are multiple arguments, use a tuple
> let action3 = create_action(cx,
> let action3 = create_action(
> |input: &(usize, String)| async { todo!() }
> );
> ```
@@ -41,7 +41,7 @@ async fn add_todo_request(new_title: &str) -> Uuid {
So in this case, all we need to do to create an action is
```rust
let add_todo_action = create_action(cx, |input: &String| {
let add_todo_action = create_action(|input: &String| {
let input = input.to_owned();
async move { add_todo_request(&input).await }
});
@@ -66,9 +66,9 @@ let todo_id = add_todo_action.value(); // RwSignal<Option<Uuid>>
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
```rust
let input_ref = create_node_ref::<Input>(cx);
let input_ref = create_node_ref::<Input>();
view! { cx,
view! {
<form
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
@@ -91,9 +91,9 @@ view! { cx,
Now, theres a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, youll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Dont worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/13-actions-0-5-8xk35v?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<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>
<iframe src="https://codesandbox.io/p/sandbox/13-actions-0-5-8xk35v?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -116,10 +116,10 @@ async fn add_todo(text: &str) -> Uuid {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> 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| {
let add_todo = create_action(|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
@@ -133,9 +133,9 @@ fn App(cx: Scope) -> impl IntoView {
let pending = add_todo.pending();
let todo_id = add_todo.value();
let input_ref = create_node_ref::<Input>(cx);
let input_ref = create_node_ref::<Input>();
view! { cx,
view! {
<form
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
@@ -168,7 +168,7 @@ fn App(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```

View File

@@ -0,0 +1,74 @@
# Deployment
There are as many ways to deploy a web application as there are developers, let alone applications. But there are a couple useful tips to keep in mind when deploying an app.
## General Advice
1. Remember: Always deploy Rust apps built in `--release` mode, not debug mode. This has a huge effect on both performance and binary size.
2. Test locally in release mode as well. The framework applies certain optimizations in release mode that it does not apply in debug mode, so its possible for bugs to surface at this point. (If your app behaves differently or you do encounter a bug, its likely a framework-level bug and you should open a GitHub issue with a reproduction.)
> We asked users to submit their deployment setups to help with this chapter. Ill quote from them below, but you can read the full thread [here](https://github.com/leptos-rs/leptos/issues/1152).
## Deploying a Client-Side-Rendered App
If youve been building an app that only uses client-side rendering, working with Trunk as a dev server and build tool, the process is quite easy.
```bash
trunk build --release
```
`trunk build` will create a number of build artifacts in a `dist/` directory. Publishing `dist` somewhere online should be all you need to deploy your app. This should work very similarly to deploying any JavaScript application.
> Read more: [Deploying to Vercel with GitHub Actions](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1577861900).
## Deploying a Full-Stack App
The most popular way for people to deploy full-stack apps built with `cargo-leptos` is to use a cloud hosting service that supports deployment via a Docker build. Heres a sample `Dockerfile`, which is based on the one we use to deploy the Leptos website.
```dockerfile
# Get started with a build env with Rust nightly
FROM rustlang/rust:nightly-bullseye as builder
# If youre using stable, use this instead
# FROM rust:1.70-bullseye as builder
# Install cargo-binstall, which makes it easier to install other
# cargo extensions like cargo-leptos
RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
RUN cp cargo-binstall /usr/local/cargo/bin
# Install cargo-leptos
RUN cargo binstall cargo-leptos -y
# Add the WASM target
RUN rustup target add wasm32-unknown-unknown
# Make an /app dir, which everything will eventually live in
RUN mkdir -p /app
WORKDIR /app
COPY . .
# Build the app
RUN cargo leptos build --release -vv
FROM rustlang/rust:nightly-bullseye as runner
# Copy the server binary to the /app directory
COPY --from=builder /app/target/release/leptos-start /app/
# /target/site contains our JS/WASM/CSS, etc.
COPY --from=builder /app/target/site /app/site
# Copy Cargo.toml if its needed at runtime
COPY --from=builder /app/Cargo.toml /app/
WORKDIR /app
# Set any required env variables and
ENV RUST_LOG="info"
ENV APP_ENVIRONMENT="production"
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
# Run the server
CMD ["/app/leptos_start"]
```
> Read more: [`gnu` and `musl` build files for Leptos apps](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1634916088).

View File

@@ -7,12 +7,12 @@ As you build components you may occasionally find yourself wanting to “project
Consider the following:
```rust
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn(Scope) -> IV + 'static,
F: Fn() -> IV + 'static,
IV: IntoView,
{
view! { cx,
view! {
<Suspense
fallback=|| ()
>
@@ -22,7 +22,7 @@ where
when=move || todo!()
fallback=fallback
>
{children(cx)}
{children()}
</Show>
</Suspense>
}
@@ -50,18 +50,16 @@ If you want to really understand the issue here, it may help to look at the expa
```rust
Suspense(
cx,
::leptos::component_props_builder(&Suspense)
.fallback(|| ())
.children({
// fallback and children are moved into this closure
Box::new(move |cx| {
Box::new(move || {
{
// fallback and children captured here
leptos::Fragment::lazy(|| {
vec![
(Show(
cx,
::leptos::component_props_builder(&Show)
.when(|| true)
// but fallback is moved into Show here
@@ -70,7 +68,7 @@ Suspense(
.children(children)
.build(),
)
.into_view(cx)),
.into_view()),
]
})
}
@@ -91,22 +89,22 @@ We can solve this problem by using the [`store_value`](https://docs.rs/leptos/la
In this case, its really simple:
```rust
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn(Scope) -> IV + 'static,
F: Fn() -> IV + 'static,
IV: IntoView,
{
let fallback = store_value(cx, fallback);
let children = store_value(cx, children);
view! { cx,
let fallback = store_value(fallback);
let children = store_value(children);
view! {
<Suspense
fallback=|| ()
>
<Show
when=|| todo!()
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
fallback=move || fallback.with_value(|fallback| fallback())
>
{children.with_value(|children| children(cx))}
{children.with_value(|children| children())}
</Show>
</Suspense>
}
@@ -125,9 +123,9 @@ Consider this example
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
let name = "Alice".to_string();
view! { cx,
view! {
<Outer>
<Inner>
<Inmost name=name.clone()/>
@@ -137,18 +135,18 @@ pub fn App(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
children(cx)
pub fn Outer(ChildrenFn) -> impl IntoView {
children()
}
#[component]
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
children(cx)
pub fn Inner(ChildrenFn) -> impl IntoView {
children()
}
#[component]
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
view! { cx,
pub fn Inmost(ng) -> impl IntoView {
view! {
<p>{name}</p>
}
}
@@ -165,7 +163,7 @@ Its captured through multiple levels of children that need to run more than o
In this case, the `clone:` syntax comes in handy. Calling `clone:name` will clone `name` _before_ moving it into `<Inner/>`s children, which solves our ownership issue.
```rust
view! { cx,
view! {
<Outer>
<Inner clone:name>
<Inmost name=name.clone()/>

View File

@@ -14,10 +14,10 @@ This allows you to write components like this:
```rust
#[component]
fn Home(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn Home() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! { cx,
view! {
<main class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
@@ -36,7 +36,7 @@ fn Home(cx: Scope) -> impl IntoView {
}
```
It can be a little complicated to set up the Tailwind integration at first, but you can check out our two examples of how to use Tailwind with a [client-side-rendered `trunk` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr_trunk) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind). `cargo-leptos` also has some [built-in Tailwind support](https://github.com/leptos-rs/cargo-leptos#site-parameters) that you can use as an alternative to Tailwinds CLI.
It can be a little complicated to set up the Tailwind integration at first, but you can check out our two examples of how to use Tailwind with a [client-side-rendered `trunk` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_csr) or with a [server-rendered `cargo-leptos` application](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind_actix). `cargo-leptos` also has some [built-in Tailwind support](https://github.com/leptos-rs/cargo-leptos#site-parameters) that you can use as an alternative to Tailwinds CLI.
## Stylers: Compile-time CSS Extraction
@@ -48,9 +48,9 @@ This allows you to write components like this:
use stylers::style;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
let styler_class = style! { "App",
#two{
##two{
color: blue;
}
div.one{
@@ -74,7 +74,7 @@ pub fn App(cx: Scope) -> impl IntoView {
}
};
view! { cx, class = styler_class,
view! { class = styler_class,
<div class="one">
<h1 id="two">"Hello"</h1>
<h2>"World"</h2>
@@ -93,7 +93,7 @@ pub fn App(cx: Scope) -> impl IntoView {
use styled::style;
#[component]
pub fn MyComponent(cx: Scope) -> impl IntoView {
pub fn MyComponent() -> impl IntoView {
let styles = style!(
div {
background-color: red;
@@ -101,7 +101,7 @@ pub fn MyComponent(cx: Scope) -> impl IntoView {
}
);
styled::view! { cx, styles,
styled::view! { styles,
<div>"This text should be red with white text."</div>
}
}

489
docs/book/src/islands.md Normal file
View File

@@ -0,0 +1,489 @@
# Guide: Islands
Leptos 0.5 introduces the new `experimental-islands` feature. This guide will walk through the islands feature and core concepts, while implementing a demo app using the islands architecture.
## The Islands Architecture
The dominant JavaScript frontend frameworks (React, Vue, Svelte, Solid, Angular) all originated as frameworks for building client-rendered single-page apps (SPAs). The initial page load is rendered to HTML, then hydrated, and subsequent navigations are handled directly in the client. (Hence “single page”: everything happens from a single page load from the server, even if there is client-side routing later.) Each of these frameworks later added server-side rendering to improve initial load times, SEO, and user experience.
This means that by default, the entire app is interactive. It also means that the entire app has to be shipped to the client as JavaScript in order to be hydrated. Leptos has followed this same pattern.
> You can read more in the chapters on [server-side rendering](./ssr/22_life_cycle.md).
But its also possible to work in the opposite direction. Rather than taking an entirely-interactive app, rendering it to HTML on the server, and then hydrating it in the browser, you can begin with a plain HTML page and add small areas of interactivity. This is the traditional format for any website or app before the 2010s: your browser makes a series of requests to the server and returns the HTML for each new page in response. After the rise of “single-page apps” (SPA), this approach has sometimes become known as a “multi-page app” (MPA) by comparison.
The phrase “islands architecture” has emerged recently to describe the approach of beginning with a “sea” of server-rendered HTML pages, and adding “islands” of interactivity throughout the page.
> ### Additional Reading
>
> The rest of this guide will look at how to use islands with Leptos. For more background on the approach in general, check out some of the articles below:
>
> - Jason Miller, [“Islands Architecture”](https://jasonformat.com/islands-architecture/), Jason Miller
> - Ryan Carniato, [“Islands & Server Components & Resumability, Oh My!”](https://dev.to/this-is-learning/islands-server-components-resumability-oh-my-319d)
> - [“Islands Architectures”](https://www.patterns.dev/posts/islands-architecture) on patterns.dev
> - [Astro Islands](https://docs.astro.build/en/concepts/islands/)
## Activating Islands Mode
Lets start with a fresh `cargo-leptos` app:
```bash
cargo leptos new --git leptos-rs/start
```
> Im using Actix because I like it. Feel free to use Axum; there should be approximately no server-specific differences in this guide.
Im just going to run
```bash
cargo leptos build
```
in the background while I fire up my editor and keep writing.
The first thing Ill do is to add the `experimental-islands` feature in my `Cargo.toml`. I need to add this to both `leptos` and `leptos_actix`:
```toml
leptos = { version = "0.5", features = ["nightly", "experimental-islands"] }
leptos_actix = { version = "0.5", optional = true, features = [
"experimental-islands",
] }
```
Next Im going to modify the `hydrate` function exported from `src/lib.rs`. Im going to remove the line that calls `leptos::mount_to_body(App)` and replace it with
```rust
leptos::leptos_dom::HydrationCtx::stop_hydrating();
```
Each “island” we create will actually act as its own entrypoint, so our `hydrate()` function just says “okay, hydrations done now.”
Okay, now fire up your `cargo leptos watch` and go to [`http://localhost:3000`](http://localhost:3000) (or wherever).
Click the button, and...
Nothing happens!
Perfect.
## Using Islands
Nothing happens because weve just totally inverted the mental model of our app. Rather than being interactive by default and hydrating everything, the app is now plain HTML by default, and we need to opt into interactivity.
This has a big effect on WASM binary sizes: if I compile in release mode, this app is a measly 24kb of WASM (uncompressed), compared to 355kb in non-islands mode. (355kb is quite large for a “Hello, world!” Its really just all the code related to client-side routing, which isnt being used in the demo.)
When we click the button, nothing happens, because our whole page is static.
So how do we make something happen?
Lets turn the `HomePage` component into an island!
Here was the non-interactive version:
```rust
#[component]
fn HomePage() -> impl IntoView {
// Creates a reactive value to update the button
let (count, set_count) = create_signal(0);
let on_click = move |_| set_count.update(|count| *count += 1);
view! {
<h1>"Welcome to Leptos!"</h1>
<button on:click=on_click>"Click Me: " {count}</button>
}
}
```
Heres the interactive version:
```rust
#[island]
fn HomePage() -> impl IntoView {
// Creates a reactive value to update the button
let (count, set_count) = create_signal(0);
let on_click = move |_| set_count.update(|count| *count += 1);
view! {
<h1>"Welcome to Leptos!"</h1>
<button on:click=on_click>"Click Me: " {count}</button>
}
}
```
Now when I click the button, it works!
The `#[island]` macro works exactly like the `#[component]` macro, except that in islands mode, it designates this as an interactive island. If we check the binary size again, this is 166kb uncompressed in release mode; much larger than the 24kb totally static version, but much smaller than the 355kb fully-hydrated version.
If you open up the source for the page now, youll see that your `HomePage` island has been rendered as a special `<leptos-island>` HTML element which specifies which component should be used to hydrate it:
```html
<leptos-island data-component="HomePage" data-hkc="0-0-0">
<h1 data-hk="0-0-2">Welcome to Leptos!</h1>
<button data-hk="0-0-3">
Click Me:
<!-- <DynChild> -->11<!-- </DynChild> -->
</button>
</leptos-island>
```
The typical Leptos hydration keys and markers are only present inside the island, only the island is hydrated.
## Using Islands Effectively
Remember that _only_ code within an `#[island]` needs to be compiled to WASM and shipped to the browser. This means that islands should be as small and specific as possible. My `HomePage`, for example, would be better broken apart into a regular component and an island:
```rust
#[component]
fn HomePage() -> impl IntoView {
view! {
<h1>"Welcome to Leptos!"</h1>
<Counter/>
}
}
#[island]
fn Counter() -> impl IntoView {
// Creates a reactive value to update the button
let (count, set_count) = create_signal(0);
let on_click = move |_| set_count.update(|count| *count += 1);
view! {
<button on:click=on_click>"Click Me: " {count}</button>
}
}
```
Now the `<h1>` doesnt need to be included in the client bundle, or hydrated. This seems like a silly distinction now; but note that you can now add as much inert HTML content as you want to the `HomePage` itself, and the WASM binary size will remain exactly the same.
In regular hydration mode, your WASM binary size grows as a function of the size/complexity of your app. In islands mode, your WASM binary grows as a function of the amount of interactivity in your app. You can add as much non-interactive content as you want, outside islands, and it will not increase that binary size.
## Unlocking Superpowers
So, this 50% reduction in WASM binary size is nice. But really, whats the point?
The point comes when you combine two key facts:
1. Code inside `#[component]` functions now _only_ runs on the server.
2. Children and props can be passed from the server to islands, without being included in the WASM binary.
This means you can run server-only code directly in the body of a component, and pass it directly into the children. Certain tasks that take a complex blend of server functions and Suspense in fully-hydrated apps can be done inline in islands.
Were going to rely on a third fact in the rest of this demo:
3. Context can be passed between otherwise-independent islands.
So, instead of our counter demo, lets make something a little more fun: a tabbed interface that reads data from files on the server.
## Passing Server Children to Islands
One of the most powerful things about islands is that you can pass server-rendered children into an island, without the island needing to know anything about them. Islands hydrate their own content, but not children that are passed to them.
As Dan Abramov of React put it (in the very similar context of RSCs), islands arent really islands: theyre donuts. You can pass server-only content directly into the “donut hole,” as it were, allowing you to create tiny atolls of interactivity, surrounded on _both_ sides by the sea of inert server HTML.
> In the demo code included below, I added some styles to show all server content as a light-blue “sea,” and all islands as light-green “land.” Hopefully that will help picture what Im talking about!
To continue with the demo: Im going to create a `Tabs` component. Switching between tabs will require some interactivity, so of course this will be an island. Lets start simple for now:
```rust
#[island]
fn Tabs(labels: Vec<String>) -> impl IntoView {
let buttons = labels
.into_iter()
.map(|label| view! { <button>{label}</button> })
.collect_view();
view! {
<div style="display: flex; width: 100%; justify-content: space-between;">
{buttons}
</div>
}
}
```
Oops. This gives me an error
```
error[E0463]: can't find crate for `serde`
--> src/app.rs:43:1
|
43 | #[island]
| ^^^^^^^^^ can't find crate
```
Easy fix: lets `cargo add serde --features=derive`. The `#[island]` macro wants to pull in `serde` here because it needs to serialize and deserialize the `labels` prop.
Now lets update the `HomePage` to use `Tabs`.
```rust
#[component]
fn HomePage() -> impl IntoView {
// these are the files were going to read
let files = ["a.txt", "b.txt", "c.txt"];
// the tab labels will just be the file names
let labels = files.iter().copied().map(Into::into).collect();
view! {
<h1>"Welcome to Leptos!"</h1>
<p>"Click any of the tabs below to read a recipe."</p>
<Tabs labels/>
}
}
```
If you take a look in the DOM inspector, youll see the island is now something like
```html
<leptos-island
data-component="Tabs"
data-hkc="0-0-0"
data-props='{"labels":["a.txt","b.txt","c.txt"]}'
></leptos-island>
```
Our `labels` prop is getting serialized to JSON and stored in an HTML attribute so it can be used to hydrate the island.
Now lets add some tabs. For the moment, a `Tab` island will be really simple:
```rust
#[island]
fn Tab(index: usize, children: Children) -> impl IntoView {
view! {
<div>{children()}</div>
}
}
```
Each tab, for now will just be a `<div>` wrapping its children.
Our `Tabs` component will also get some children: for now, lets just show them all.
```rust
#[island]
fn Tabs(labels: Vec<String>, children: Children) -> impl IntoView {
let buttons = labels
.into_iter()
.map(|label| view! { <button>{label}</button> })
.collect_view();
view! {
<div style="display: flex; width: 100%; justify-content: space-around;">
{buttons}
</div>
{children()}
}
}
```
Okay, now lets go back into the `HomePage`. Were going to create the list of tabs to put into our tab box.
```rust
#[component]
fn HomePage() -> impl IntoView {
let files = ["a.txt", "b.txt", "c.txt"];
let labels = files.iter().copied().map(Into::into).collect();
let tabs = move || {
files
.into_iter()
.enumerate()
.map(|(index, filename)| {
let content = std::fs::read_to_string(filename).unwrap();
view! {
<Tab index>
<h2>{filename.to_string()}</h2>
<p>{content}</p>
</Tab>
}
})
.collect_view()
};
view! {
<h1>"Welcome to Leptos!"</h1>
<p>"Click any of the tabs below to read a recipe."</p>
<Tabs labels>
<div>{tabs()}</div>
</Tabs>
}
}
```
Uh... What?
If youre used to using Leptos, you know that you just cant do this. All code in the body of components has to run on the server (to be rendered to HTML) and in the browser (to hydrate), so you cant just call `std::fs`; it will panic, because theres no access to the local filesystem (and certainly not to the server filesystem!) in the browser. This would be a security nightmare!
Except... wait. Were in islands mode. This `HomePage` component _really does_ only run on the server. So we can, in fact, just use ordinary server code like this.
> **Is this a dumb example?** Yes! Synchronously reading from three different local files in a `.map()` is not a good choice in real life. The point here is just to demonstrate that this is, definitely, server-only content.
Go ahead and create three files in the root of the project called `a.txt`, `b.txt`, and `c.txt`, and fill them in with whatever content youd like.
Refresh the page and you should see the content in the browser. Edit the files and refresh again; it will be updated.
You can pass server-only content from a `#[component]` into the children of an `#[island]`, without the island needing to know anything about how to access that data or render that content.
**This is really important.** Passing server `children` to islands means that you can keep islands small. Ideally, you dont want to slap and `#[island]` around a whole chunk of your page. You want to break that chunk out into an interactive piece, which can be an `#[island]`, and a bunch of additional server content that can be passed to that island as `children`, so that the non-interactive subsections of an interactive part of the page can be kept out of the WASM binary.
## Passing Context Between Islands
These arent really “tabs” yet: they just show every tab, all the time. So lets add some simple logic to our `Tabs` and `Tab` components.
Well modify `Tabs` to create a simple `selected` signal. We provide the read half via context, and set the value of the signal whenever someone clicks one of our buttons.
```rust
#[island]
fn Tabs(labels: Vec<String>, children: Children) -> impl IntoView {
let (selected, set_selected) = create_signal(0);
provide_context(selected);
let buttons = labels
.into_iter()
.enumerate()
.map(|(index, label)| view! {
<button on:click=move |_| set_selected(index)>
{label}
</button>
})
.collect_view();
// ...
```
And lets modify the `Tab` island to use that context to show or hide itself:
```rust
#[island]
fn Tab(children: Children) -> impl IntoView {
let selected = expect_context::<ReadSignal<usize>>();
view! {
<div style:display=move || if selected() {
"block"
} else {
"none"
}>
// ...
```
Now the tabs behave exactly as Id expect. `Tabs` passes the signal via context to each `Tab`, which uses it to determine whether it should be open or not.
> Thats why in `HomePage`, I made `let tabs = move ||` a function, and called it like `{tabs()}`: creating the tabs lazily this way meant that the `Tabs` island would already have provided the `selected` context by the time each `Tab` went looking for it.
Our complete tabs demo is about 220kb uncompressed: not the smallest demo in the world, but still about a third smaller than the counter button! Just for kicks, I built the same demo without islands mode, using `#[server]` functions and `Suspense`. and it was 429kb. So again, this was about a 50% savings in binary size. And this app includes quite minimal server-only content: remember that as we add additional server-only components and pages, this 220 will not grow.
## Overview
This demo may seem pretty basic. It is. But there are a number of immediate takeaways:
- **50% WASM binary size reduction**, which means measurable improvements in time to interactivity and initial load times for clients.
- **Reduced HTML page size.** This one is less obvious, but its true and important: HTML generated from `#[component]`s doesnt need all the hydration IDs and other boilerplate added.
- **Reduced data serialization costs.** Creating a resource and reading it on the client means you need to serialize the data, so it can be used for hydration. If youve also read that data to create HTML in a `Suspense`, you end up with “double data,” i.e., the same exact data is both rendered to HTML and serialized as JSON, increasing the size of responses, and therefore slowing them down.
- **Easily use server-only APIs** inside a `#[component]` as if it were a normal, native Rust function running on the server—which, in islands mode, it is!
- **Reduced `#[server]`/`create_resource`/`Suspense` boilerplate** for loading server data.
## Future Exploration
The `experimental-islands` feature included in 0.5 reflects work at the cutting edge of what frontend web frameworks are exploring right now. As it stands, our islands approach is very similar to Astro (before its recent View Transitions support): it allows you to build a traditional server-rendered, multi-page app and pretty seamlessly integrate islands of interactivity.
There are some small improvements that will be easy to add. For example, we can do something very much like Astro's View Transitions approach:
- add client-side routing for islands apps by fetching subsequent navigations from the server and replacing the HTML document with the new one
- add animated transitions between the old and new document using the View Transitions API
- support explicit persistent islands, i.e., islands that you can mark with unique IDs (something like `persist:searchbar` on the component in the view), which can be copied over from the old to the new document without losing their current state
There are other, larger architectural changes that Im [not sold on yet](https://github.com/leptos-rs/leptos/issues/1830).
## Additional Information
Check out the [islands PR](https://github.com/leptos-rs/leptos/pull/1660), [roadmap](https://github.com/leptos-rs/leptos/issues/1830), and [Hackernews demo](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_islands_axum) for additional discussion.
## Demo Code
```rust
use leptos::*;
use leptos_router::*;
#[component]
pub fn App() -> impl IntoView {
view! {
<Router>
<main style="background-color: lightblue; padding: 10px">
<Routes>
<Route path="" view=HomePage/>
</Routes>
</main>
</Router>
}
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
let files = ["a.txt", "b.txt", "c.txt"];
let labels = files.iter().copied().map(Into::into).collect();
let tabs = move || {
files
.into_iter()
.enumerate()
.map(|(index, filename)| {
let content = std::fs::read_to_string(filename).unwrap();
view! {
<Tab index>
<div style="background-color: lightblue; padding: 10px">
<h2>{filename.to_string()}</h2>
<p>{content}</p>
</div>
</Tab>
}
})
.collect_view()
};
view! {
<h1>"Welcome to Leptos!"</h1>
<p>"Click any of the tabs below to read a recipe."</p>
<Tabs labels>
<div>{tabs()}</div>
</Tabs>
}
}
#[island]
fn Tabs(labels: Vec<String>, children: Children) -> impl IntoView {
let (selected, set_selected) = create_signal(0);
provide_context(selected);
let buttons = labels
.into_iter()
.enumerate()
.map(|(index, label)| {
view! {
<button on:click=move |_| set_selected(index)>
{label}
</button>
}
})
.collect_view();
view! {
<div
style="display: flex; width: 100%; justify-content: space-around;\
background-color: lightgreen; padding: 10px;"
>
{buttons}
</div>
{children()}
}
}
#[island]
fn Tab(index: usize, children: Children) -> impl IntoView {
let selected = expect_context::<ReadSignal<usize>>();
view! {
<div
style:background-color="lightgreen"
style:padding="10px"
style:display=move || if selected() == index {
"block"
} else {
"none"
}
>
{children()}
</div>
}
}
```

View File

@@ -30,13 +30,13 @@ Theres a very simple way to determine whether you should use a capital-S `<Sc
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:
`<Body/>` and `<Html/>` both also have `attributes` props which can be used to set any number of additional attributes on them via the `attr:` syntax:
```rust
<Html
lang="he"
dir="rtl"
attributes=AdditionalAttributes::from(vec![("data-theme", "dark")])
attr:data-theme="dark"
/>
```

View File

@@ -3,12 +3,13 @@
[`<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.
> **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")]
@@ -17,14 +18,14 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
#[component]
fn AddTodo(cx: Scope) -> impl IntoView {
let add_todo = create_server_action::<AddTodo>(cx);
fn AddTodo() -> impl IntoView {
let add_todo = create_server_action::<AddTodo>();
// 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,
view! {
<ActionForm action=add_todo>
<label>
"Add a Todo"
@@ -36,6 +37,7 @@ fn AddTodo(cx: Scope) -> impl IntoView {
}
}
```
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
@@ -53,4 +55,4 @@ let on_submit = move |ev| {
ev.prevent_default();
}
}
```
```

View File

@@ -9,10 +9,10 @@ Hidden behind the whole reactive DOM renderer that weve seen so far is a func
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
```rust
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(0);
create_effect(cx, move |_| {
create_effect(move |_| {
// immediately prints "Value: 0" and subscribes to `a`
log::debug!("Value: {}", a());
});
@@ -42,15 +42,14 @@ While theyre not a “zero-cost abstraction” in the most technical sense—
Imagine that Im creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
```rust
let (first, set_first) = create_signal(cx, String::new());
let (last, set_last) = create_signal(cx, String::new());
let (use_last, set_use_last) = create_signal(cx, true);
let (first, set_first) = create_signal(String::new());
let (last, set_last) = create_signal(String::new());
let (use_last, set_use_last) = create_signal(true);
// this will add the name to the log
// any time one of the source signals changes
create_effect(cx, move |_| {
create_effect(move |_| {
log(
cx,
if use_last() {
format!("{} {}", first(), last())
} else {
@@ -77,9 +76,9 @@ If you need to synchronize some reactive value with the non-reactive world outsi
Weve managed to get this far without mentioning effects because theyre built into the Leptos DOM renderer. Weve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
view! { cx,
view! {
<p>{count}</p>
}
```
@@ -87,13 +86,13 @@ view! { cx,
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// create a DOM element
let p = create_element("p");
// create an effect to reactively update the text
create_effect(cx, move |prev_value| {
create_effect(move |prev_value| {
// first, access the signals value and convert it to a string
let text = count().to_string();
@@ -109,9 +108,36 @@ create_effect(cx, move |prev_value| {
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
## Explicit, Cancelable Tracking with `watch`
<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>
In addition to `create_effect`, Leptos provides a [`watch`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.watch.html) function, which can be used for two main purposes:
1. Separating tracking and responding to changes by explicitly passing in a set of values to track.
2. Canceling tracking by calling a stop function.
Like `create_resource`, `watch` takes a first argument, which is reactively tracked, and a second, which is not. Whenever a reactive value in its `deps` argument is changed, the `callback` is run. `watch` returns a function that can be called to stop tracking the dependencies.
```rust
let (num, set_num) = create_signal(0);
let stop = watch(
move || num.get(),
move |num, prev_num, _| {
log::debug!("Number: {}; Prev: {:?}", num, prev_num);
},
false,
);
set_num.set(1); // > "Number: 1; Prev: Some(0)"
stop(); // stop watching
set_num.set(2); // (nothing happens)
```
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/14-effect-0-5-d6hkch?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<iframe src="https://codesandbox.io/p/sandbox/14-effect-0-5-d6hkch?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -121,30 +147,30 @@ use leptos::html::Input;
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> impl IntoView {
// Just making a visible log here
// You can ignore this...
let log = create_rw_signal::<Vec<String>>(cx, vec![]);
let log = create_rw_signal::<Vec<String>>(vec![]);
let logged = move || log().join("\n");
provide_context(cx, log);
provide_context(log);
view! { cx,
view! {
<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);
fn CreateAnEffect() -> impl IntoView {
let (first, set_first) = create_signal(String::new());
let (last, set_last) = create_signal(String::new());
let (use_last, set_use_last) = create_signal(true);
// this will add the name to the log
// any time one of the source signals changes
create_effect(cx, move |_| {
create_effect(move |_| {
log(
cx,
if use_last() {
format!("{} {}", first(), last())
} else {
@@ -153,7 +179,7 @@ fn CreateAnEffect(cx: Scope) -> impl IntoView {
)
});
view! { cx,
view! {
<h1><code>"create_effect"</code> " Version"</h1>
<form>
<label>
@@ -179,14 +205,14 @@ fn CreateAnEffect(cx: Scope) -> impl IntoView {
}
#[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);
fn ManualVersion() -> impl IntoView {
let first = create_node_ref::<Input>();
let last = create_node_ref::<Input>();
let use_last = create_node_ref::<Input>();
let mut prev_name = String::new();
let on_change = move |_| {
log(cx, " listener");
log(" listener");
let first = first.get().unwrap();
let last = last.get().unwrap();
let use_last = use_last.get().unwrap();
@@ -197,12 +223,12 @@ fn ManualVersion(cx: Scope) -> impl IntoView {
};
if this_one != prev_name {
log(cx, &this_one);
log(&this_one);
prev_name = this_one;
}
};
view! { cx,
view! {
<h1>"Manual Version"</h1>
<form on:change=on_change>
<label>
@@ -229,12 +255,12 @@ fn ManualVersion(cx: Scope) -> impl IntoView {
}
#[component]
fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
let (my_value, set_my_value) = create_signal(cx, String::new());
fn EffectVsDerivedSignal() -> impl IntoView {
let (my_value, set_my_value) = create_signal(String::new());
// Don't do this.
/*let (my_optional_value, set_optional_my_value) = create_signal(cx, Option::<String>::None);
/*let (my_optional_value, set_optional_my_value) = create_signal(Option::<String>::None);
create_effect(cx, move |_| {
create_effect(move |_| {
if !my_value.get().is_empty() {
set_optional_my_value(Some(my_value.get()));
} else {
@@ -246,7 +272,7 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
let my_optional_value =
move || (!my_value.with(String::is_empty)).then(|| Some(my_value.get()));
view! { cx,
view! {
<input
prop:value=my_value
on:input= move |ev| set_my_value(event_target_value(&ev))
@@ -258,7 +284,7 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
<code>
<Show
when=move || my_optional_value().is_some()
fallback=|cx| view! { cx, "None" }
fallback=|| view! { "None" }
>
"Some(\"" {my_optional_value().unwrap()} "\")"
</Show>
@@ -267,12 +293,10 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
}
}
/*#[component]
#[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>,
children: Box<dyn Fn() -> 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
@@ -280,26 +304,25 @@ pub fn Show<F, W, IV>(
) -> impl IntoView
where
W: Fn() -> bool + 'static,
F: Fn(Scope) -> IV + 'static,
F: Fn() -> IV + 'static,
IV: IntoView,
{
let memoized_when = create_memo(cx, move |_| when());
let memoized_when = create_memo(move |_| when());
move || match memoized_when.get() {
true => children(cx).into_view(cx),
false => fallback(cx).into_view(cx),
true => children().into_view(),
false => fallback().into_view(),
}
}*/
}
fn log(cx: Scope, msg: impl std::fmt::Display) {
let log = use_context::<RwSignal<Vec<String>>>(cx).unwrap();
fn log(msg: impl std::fmt::Display) {
let log = use_context::<RwSignal<Vec<String>>>().unwrap();
log.update(|log| log.push(msg.to_string()));
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -6,7 +6,7 @@ application. It sometimes looks a little silly:
```rust
// a signal holds a value, and can be updated
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// a derived signal is a function that accesses other signals
let double_count = move || count() * 2;
@@ -19,11 +19,11 @@ let text = move || if count_is_odd() {
// an effect automatically tracks the signals it depends on
// and reruns when they change
create_effect(cx, move |_| {
log!("text = {}", text());
create_effect(move |_| {
logging::log!("text = {}", text());
});
view! { cx,
view! {
<p>{move || text().to_uppercase()}</p>
}
```
@@ -53,12 +53,12 @@ Take our typical `<SimpleCounter/>` example in its simplest form:
```rust
#[component]
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
pub fn SimpleCounter() -> impl IntoView {
let (value, set_value) = create_signal(0);
let increment = move |_| set_value.update(|value| *value += 1);
view! { cx,
view! {
<button on:click=increment>
{value}
</button>

View File

@@ -14,17 +14,17 @@ There are four basic signal operations:
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);
let (count, set_count) = create_signal(0);
set_count(1);
log!(count());
logging::log!(count());
```
is the same as
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
set_count.set(1);
log!(count.get());
logging::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)`.
@@ -36,7 +36,7 @@ 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());
let (names, set_names) = create_signal(Vec::new());
if names().is_empty() {
set_names(vec!["Alice".to_string()]);
}
@@ -47,7 +47,7 @@ In terms of logic, this is simple enough, but its hiding some significant ine
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());
let (names, set_names) = create_signal(Vec::new());
if names.with(|names| names.is_empty()) {
set_names.update(|names| names.push("Alice".to_string()));
}
@@ -63,40 +63,74 @@ 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.
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 unnecessary closure.
There are some helper macros to make using `.with()` and `.update()` easier to use, especially when using multiple signals.
```rust
let (first, _) = create_signal("Bob".to_string());
let (middle, _) = create_signal("J.".to_string());
let (last, _) = create_signal("Smith".to_string());
```
If you wanted to concatenate these 3 signals together without unnecessary cloning, you would have to write something like:
```rust
let name = move || {
first.with(|first| {
middle.with(|middle| last.with(|last| format!("{first} {middle} {last}")))
})
};
```
Which is very long and annoying to write.
Instead, you can use the `with!` macro to get references to all the signals at the same time.
```rust
let name = move || with!(|first, middle, last| format!("{first} {middle} {last}"));
```
This expands to the same thing as above. Take a look at the `with!` docs for more info, and the corresponding macros `update!`, `with_value!` and `update_value!`.
## 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 (count, set_count) = create_signal(1);
let derived_signal_double_count = move || count() * 2;
let memoized_double_count = create_memo(cx, move |_| count() * 2);
let memoized_double_count = create_memo(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 (first_name, set_first_name) = create_signal("Bridget".to_string());
let (last_name, set_last_name) = create_signal("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);
let (age, set_age) = create_signal(32);
let (favorite_number, set_favorite_number) = create_signal(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.

View File

@@ -33,8 +33,8 @@ use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
pub fn App() -> impl IntoView {
view! {
<Router>
<nav>
/* ... */
@@ -58,8 +58,8 @@ use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
pub fn App() -> impl IntoView {
view! {
<Router>
<nav>
/* ... */
@@ -83,21 +83,62 @@ The `path` can include
- dynamic, named parameters beginning with a colon (`/:id`),
- and/or a wildcard beginning with an asterisk (`/user/*any`)
The `view` is a function that takes a `Scope` and returns a view.
The `view` is a function that returns a view. Any component with no props works here, as does a closure that returns some view.
```rust
<Routes>
<Route path="/" view=Home/>
<Route path="/users" view=Users/>
<Route path="/users/:id" view=UserProfile/>
<Route path="/*any" view=NotFound/>
<Route path="/*any" view=|| view! { <h1>"Not Found"</h1> }/>
</Routes>
```
> `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/> }`.
> `view` takes a `Fn() -> impl IntoView`. If a component has no props, it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|| view! { <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?
## Conditional Routes
`leptos_router` is based on the assumption that you have one and only one `<Routes/>` component in your app. It uses this to generate routes on the server side, optimize route matching by caching calculated branches, and render your application.
You should not conditionally render `<Routes/>` using another component like `<Show/>` or `<Suspense/>`.
```rust
// ❌ don't do this!
view! {
<Show when=|| is_loaded() fallback=|| view! { <p>"Loading"</p> }>
<Routes>
<Route path="/" view=Home/>
</Routes>
</Show>
}
```
Instead, you can use nested routing to render your `<Routes/>` once, and conditionally render the router outlet:
```rust
// ✅ do this instead!
view! {
<Routes>
// parent route
<Route path="/" view=move || {
view! {
// only show the outlet if data have loaded
<Show when=|| is_loaded() fallback=|| view! { <p>"Loading"</p> }>
<Outlet/>
</Show>
}
}>
// nested child route
<Route path="/" view=Home/>
</Route>
</Routes>
}
```
If this looks bizarre, dont worry! The next section of the book is about this kind of nested routing.

View File

@@ -4,10 +4,10 @@ We just defined the following set of routes:
```rust
<Routes>
<Route path="/" view=Home
<Route path="/users" view=Users
<Route path="/users/:id" view=UserProfile
<Route path="/*any" view=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=Home
<Route path="/users" view=Users
<Route path=":id" view=UserProfile
<Route path="/" view=Home/>
<Route path="/users" view=Users>
<Route path=":id" view=UserProfile/>
</Route>
<Route path="/*any" view=NotFound
<Route path="/*any" view=NotFound/>
</Routes>
```
@@ -39,8 +39,8 @@ Lets look back at our practical example.
```rust
<Routes>
<Route path="/users" view=Users
<Route path="/users/:id" view=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=Users
<Route path=":id" view=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=Users
<Route path=":id" view=UserProfile
<Route path="" view=NoUser
<Route path="/users" view=Users>
<Route path=":id" view=UserProfile/>
<Route path="" view=NoUser/>
</Route>
</Routes>
```
@@ -94,9 +94,9 @@ You can easily define this with nested routes
```rust
<Routes>
<Route path="/contacts" view=ContactList
<Route path=":id" view=ContactInfo
<Route path="" view=|cx| view! { cx,
<Route path="/contacts" view=ContactList>
<Route path=":id" view=ContactInfo/>
<Route path="" view=|| view! {
<p>"Select a contact to view more info."</p>
}/>
</Route>
@@ -107,13 +107,13 @@ You can go even deeper. Say you want to have tabs for each contacts address,
```rust
<Routes>
<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 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,
<Route path="" view=|| view! {
<p>"Select a contact to view more info."</p>
}/>
</Route>
@@ -135,16 +135,16 @@ Thats all! But its important to know and to remember, because its a com
```rust
#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
pub fn ContactList() -> impl IntoView {
let contacts = todo!();
view! { cx,
view! {
<div style="display: flex">
// the contact list
<For each=contacts
key=|contact| contact.id
view=|cx, contact| todo!()
>
children=|contact| todo!()
/>
// the nested child, if any
// dont forget this!
<Outlet/>
@@ -153,6 +153,43 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
}
```
## Refactoring Route Definitions
You dont need to define all your routes in one place if you dont want to. You can refactor any `<Route/>` and its children out into a separate component.
For example, you can refactor the example above to use two separate components:
```rust
#[component]
fn App() -> impl IntoView {
view! {
<Router>
<Routes>
<Route path="/contacts" view=ContactList>
<ContactInfoRoutes/>
<Route path="" view=|| view! {
<p>"Select a contact to view more info."</p>
}/>
</Route>
</Routes>
</Router>
}
}
#[component(transparent)]
fn ContactInfoRoutes() -> impl IntoView {
view! {
<Route path=":id" view=ContactInfo>
<Route path="" view=EmailAndPhone/>
<Route path="address" view=Address/>
<Route path="messages" view=Messages/>
</Route>
}
}
```
This second component is a `#[component(transparent)]`, meaning it just returns its data, not a view: in this case, it's a [`RouteDefinition`](https://docs.rs/leptos_router/latest/leptos_router/struct.RouteDefinition.html) struct, which is what the `<Route/>` returns. As long as it is marked `#[component(transparent)]`, this sub-route can be defined wherever you want, and inserted as a component into your tree of route definitions.
## Nested Routing and Performance
All of this is nice, conceptually, but again—whats the big deal?
@@ -167,9 +204,9 @@ In fact, in this case, we dont even need to rerender the `<Contact/>` compone
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple well cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so dont be surprised if theres anything you dont understand.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2)
<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>
<iframe src="https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -179,8 +216,8 @@ use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
@@ -195,28 +232,29 @@ fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<Route path="/" view=|| view! {
<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,
<Route path=":id" view=ContactInfo>
<Route path="" view=|| view! {
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<Route path="conversations" view=|| view! {
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="select-user">
"Select a user to view contact info."
</div>
@@ -229,8 +267,8 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
fn ContactList() -> impl IntoView {
view! {
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
@@ -249,9 +287,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
fn ContactInfo() -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let params = use_params_map();
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
@@ -262,7 +300,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
_ => "User not found.",
};
view! { cx,
view! {
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
@@ -278,9 +316,8 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -36,14 +36,22 @@ 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.
```rust
let params = use_params::<ContactParams>(cx);
let query = use_query::<ContactSearch>(cx);
let params = use_params::<ContactParams>();
let query = use_query::<ContactSearch>();
// id: || -> usize
let id = move || {
@@ -58,8 +66,8 @@ let id = move || {
The untyped versions return `Memo<ParamsMap>`. Again, its memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
```rust
let params = use_params_map(cx);
let query = use_query_map(cx);
let params = use_params_map();
let query = use_query_map();
// id: || -> Option<String>
let id = move || {
@@ -74,9 +82,9 @@ This can get a little messy: deriving a signal that wraps an `Option<_>` or `Res
> 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)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2)
<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>
<iframe src="https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -86,8 +94,8 @@ use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
@@ -102,28 +110,29 @@ fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<Route path="/" view=|| view! {
<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,
<Route path=":id" view=ContactInfo>
<Route path="" view=|| view! {
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<Route path="conversations" view=|| view! {
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="select-user">
"Select a user to view contact info."
</div>
@@ -136,8 +145,8 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
fn ContactList() -> impl IntoView {
view! {
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
@@ -156,9 +165,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
fn ContactInfo() -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let params = use_params_map();
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
@@ -169,7 +178,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
_ => "User not found.",
};
view! { cx,
view! {
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
@@ -185,9 +194,8 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -11,16 +11,33 @@ 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.
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., its a link to the page youre on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if its a link to the page youre currently on, you can match this attribute with a CSS selector.
## Navigating Programmatically
Your most-used methods of navigating between pages should be with `<a>` and `<form>` elements or with the enhanced `<A/>` and `<Form/>` components. Using links and forms to navigate is the best solution for accessibility and graceful degradation.
On occasion, though, youll want to navigate programmatically, i.e., call a function that can navigate to a new page. In that case, you should use the [`use_navigate`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_navigate.html) function.
```rust
let navigate = leptos_router::use_navigate();
navigate("/somewhere", Default::default());
```
> You should almost never do something like `<button on:click=move |_| navigate(/* ... */)>`. Any `on:click` that navigates should be an `<a>`, for reasons of accessibility.
The second argument here is a set of [`NavigateOptions`](https://docs.rs/leptos_router/latest/leptos_router/struct.NavigateOptions.html), which includes options to resolve the navigation relative to the current route as the `<A/>` component does, replace it in the navigation stack, include some navigation state, and maintain the current scroll state on navigation.
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2)
<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>
<iframe src="https://codesandbox.io/p/sandbox/16-router-0-5-4xp4zz?file=%2Fsrc%2Fmain.rs%3A102%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -30,8 +47,8 @@ use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
@@ -46,28 +63,29 @@ fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<Route path="/" view=|| view! {
<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,
<Route path=":id" view=ContactInfo>
<Route path="" view=|| view! {
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<Route path="conversations" view=|| view! {
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="select-user">
"Select a user to view contact info."
</div>
@@ -80,8 +98,8 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
fn ContactList() -> impl IntoView {
view! {
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
@@ -100,9 +118,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
fn ContactInfo() -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let params = use_params_map();
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
@@ -113,7 +131,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
_ => "User not found.",
};
view! { cx,
view! {
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
@@ -129,9 +147,8 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -24,15 +24,15 @@ async fn fetch_results() {
}
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
pub fn FormExample() -> impl IntoView {
// reactive access to URL query strings
let query = use_query_map(cx);
let query = use_query_map();
// search stored as ?q=
let search = move || query().get("q").cloned().unwrap_or_default();
// a resource driven by the search string
let search_results = create_resource(cx, search, fetch_results);
let search_results = create_resource(search, fetch_results);
view! { cx,
view! {
<Form method="GET" action="">
<input type="search" name="search" value=search/>
<input type="submit"/>
@@ -51,7 +51,7 @@ This is a great pattern. The data flow is extremely clear: all data flows from t
We can actually take it a step further and do something kind of clever:
```rust
view! { cx,
view! {
<Form method="GET" action="">
<input type="search" name="search" value=search
oninput="this.form.requestSubmit()"
@@ -62,9 +62,9 @@ view! { cx,
Youll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the users input as they type.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/20-form-0-5-9g7v9p?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<iframe src="https://codesandbox.io/p/sandbox/20-form-0-5-9g7v9p?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -74,13 +74,13 @@ use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<Router>
<h1><code>"<Form/>"</code></h1>
<main>
<Routes>
<Route path="" view=FormExample
<Route path="" view=FormExample/>
</Routes>
</main>
</Router>
@@ -88,14 +88,14 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
pub fn FormExample() -> impl IntoView {
// reactive access to URL query
let query = use_query_map(cx);
let query = use_query_map();
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,
view! {
// read out the URL query strings
<table>
<tr>
@@ -172,9 +172,8 @@ pub fn FormExample(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -31,9 +31,8 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
#[component]
pub fn BusyButton(cx: Scope) -> impl IntoView {
pub fn BusyButton() -> impl IntoView {
view! {
cx,
<button on:click=move |_| {
spawn_local(async {
add_todo("So much to do!".to_string()).await;
@@ -70,6 +69,18 @@ There are a few things to note about the way you define a server function, too.
- We provide the macro a path. This is a prefix for the path at which well mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
- Youll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
## Server Function URL Prefixes
You can optionally define a specific URL prefix to be used in the definition of the server function.
This is done by providing an optional 2nd argument to the `#[server]` macro.
By default the URL prefix will be `/api`, if not specified.
Here are some examples:
```rust
#[server(AddTodo)] // will use the default URL prefix of `/api`
#[server(AddTodo, "/foo")] // will use the URL prefix of `/foo`
```
## Server Function Encodings
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which well see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
@@ -100,11 +111,26 @@ In other words, you have two choices:
> **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.
## Server Functions Endpoint Paths
By default, a unique path will be generated. You can optionally define a specific endpoint path to be used in the URL. This is done by providing an optional 4th argument to the `#[server]` macro. Leptos will generate the complete path by concatenating the URL prefix (2nd argument) and the endpoint path (4th argument).
For example,
```rust
#[server(MyServerFnType, "/api", "Url", "hello")]
```
will generate a server function endpoint at `/api/hello` that accepts a POST request.
> **Can I use the same server function endpoint path with multiple encodings?**
>
> No. Different server functions must have unique paths. The `#[server]` macro automatically generates unique paths, but you need to be careful if you choose to specify the complete path manually, as the server looks up server functions by their path.
## 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.
@@ -113,7 +139,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

@@ -6,9 +6,9 @@ The server functions we looked at in the last chapter showed how to run code on
We call Leptos a “full-stack” framework, but “full-stack” is always a misnomer (after all, it never means everything from the browser to your power company.) For us, “full stack” means that your Leptos app can run in the browser, and can run on the server, and can integrate the two, drawing together the unique features available in each; as weve seen in the book so far, a button click on the browser can drive a database read on the server, both written in the same Rust module. But Leptos itself doesnt provide the server (or the database, or the operating system, or the firmware, or the electrical cables...)
Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_actix/latest/leptos_axum/)). Weve built integrations with each servers router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
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 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.
> 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
@@ -23,12 +23,12 @@ The [`extract` function in `leptos_actix`](https://docs.rs/leptos_actix/latest/l
```rust
#[server(ActixExtract, "/api")]
pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
pub async fn actix_extract() -> Result<String, ServerFnError> {
use leptos_actix::extract;
use actix_web::dev::ConnectionInfo;
use actix_web::web::{Data, Query};
extract(cx,
extract(
|search: Query<Search>, connection: ConnectionInfo| async move {
format!(
"search = {}\nconnection = {:?}",
@@ -43,15 +43,15 @@ pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
## Axum Extractors
The syntax for the `leptos_axum::extract` function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so youll need to add something to handle the error case.
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> {
pub async fn axum_extract() -> Result<String, ServerFnError> {
use axum::{extract::Query, http::Method};
use leptos_axum::extract;
extract(cx, |method: Method, res: Query<MyQuery>| async move {
extract(|method: Method, res: Query<MyQuery>| async move {
format!("{method:?} and {}", res.q)
},
)
@@ -62,7 +62,21 @@ pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
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).
The Axum `extract` function only supports extractors for which the state is `()`. If you need an extractor that uses `State`, you should use [`extract_with_state`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract_with_state.html). This requires you to provide the state. You can do this by extending the existing `LeptosOptions` state using the Axum `FromRef` pattern, which providing the state as context during render and server functions with custom handlers.
```rust
use axum::extract::FromRef;
/// Derive FromRef to allow multiple items in state, using Axums
/// SubStates pattern.
#[derive(FromRef, Debug, Clone)]
pub struct AppState{
pub leptos_options: LeptosOptions,
pub pool: SqlitePool
}
```
[Click here for an example of providing context in custom handlers](https://github.com/leptos-rs/leptos/blob/19ea6fae6aec2a493d79cc86612622d219e6eebb/examples/session_auth_axum/src/main.rs#L24-L44).
## A Note about Data-Loading Patterns

View File

@@ -8,12 +8,12 @@ Extractors provide an easy way to access request data inside server functions. L
```rust
#[server(TeaAndCookies)]
pub async fn tea_and_cookies(cx: Scope) -> Result<(), ServerFnError> {
pub async fn tea_and_cookies() -> 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);
let response = expect_context::<ResponseOptions>();
// set the HTTP status code
response.set_status(StatusCode::IM_A_TEAPOT);
@@ -35,14 +35,13 @@ Heres a simplified example from our [`session_auth_axum` example](https://git
```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)?;
let pool = pool()?;
let auth = auth()?;
// check whether the user exists
let user: User = User::get_from_username(username, &pool)
@@ -60,7 +59,7 @@ pub async fn login(
auth.remember_user(remember.is_some());
// and redirect to the home page
leptos_axum::redirect(cx, "/");
leptos_axum::redirect("/");
Ok(())
}
// if not, return an error

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 the major ways 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) (and a partially-blocked variant)
## Synchronous Rendering
@@ -62,7 +67,7 @@ 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.
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. It is triggered by setting `ssr=SsrMode::PartiallyBlocked` on a route, and depending on blocking resources within the view. If one of the `<Suspense/>` components 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, similar to the `SsrMode::OutOfOrder` default.
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.
@@ -79,7 +84,7 @@ 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=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
@@ -105,14 +110,14 @@ With blocking resources, I can do something like this:
```rust
#[component]
pub fn BlogPost(cx: Scope) -> impl IntoView {
let post_data = create_blocking_resource(cx, /* load blog post */);
let comment_data = create_resource(cx, /* load blog post */);
view! { cx,
pub fn BlogPost() -> impl IntoView {
let post_data = create_blocking_resource(/* load blog post */);
let comment_data = create_resource(/* load blog post */);
view! {
<Suspense fallback=|| ()>
{move || {
post_data.with(cx, |data| {
view! { cx,
post_data.with(|data| {
view! {
<Title text=data.title/>
<Meta name="description" content=data.excerpt/>
<article>
@@ -129,4 +134,23 @@ pub fn BlogPost(cx: Scope) -> impl IntoView {
}
```
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. Meta tags and other head elements awaiting the blocking resource will be rendered before the stream is sent.
Combined with the following route definition, which uses `SsrMode::PartiallyBlocked`, the blocking resource will be fully rendered on the server side, making it accessible to users who disable WebAssembly or JavaScript.
```rust
<Routes>
// Well load the home page with out-of-order streaming and <Suspense/>
<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=Post
ssr=SsrMode::PartiallyBlocked
/>
</Routes>
```
The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.

View File

@@ -8,8 +8,8 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
leptos::log!("where do I run?");
pub fn App() -> impl IntoView {
logging::log!("where do I run?");
// ... whatever
}
```
@@ -57,15 +57,15 @@ One way to create a bug is by creating a mismatch between the HTML thats sent
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
let data = if cfg!(target_arch = "wasm32") {
vec![0, 1, 2]
} else {
vec![]
};
data.into_iter()
.map(|value| view! { cx, <span>{value}</span> })
.collect_view(cx)
.map(|value| view! { <span>{value}</span> })
.collect_view()
}
```
@@ -87,49 +87,12 @@ 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 |_| {
create_effect(move |_| {
// do something like reading from localStorage
request_animation_frame(move || set_loaded(true));
});
@@ -163,10 +126,10 @@ For example, say that I want to store something in the browsers `localStorage
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
use gloo_storage::Storage;
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
logging::log!("{storage:?}");
}
```
@@ -176,11 +139,11 @@ But if I wrap it in an effect...
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
use gloo_storage::Storage;
create_effect(cx, move |_| {
create_effect(move |_| {
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
logging::log!("{storage:?}");
});
}
```

View File

@@ -14,8 +14,8 @@ For example, instead of embedding logic in a component directly like this:
```rust
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
pub fn TodoApp() -> impl IntoView {
let (todos, set_todos) = create_signal(vec![Todo { /* ... */ }]);
// ⚠️ this is hard to test because it's embedded in the component
let num_remaining = move || todos.with(|todos| {
todos.iter().filter(|todo| !todo.completed).sum()
@@ -37,14 +37,14 @@ impl Todos {
#[cfg(test)]
mod tests {
#[test]
fn test_remaining {
fn test_remaining() {
// ...
}
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
pub fn TodoApp() -> impl IntoView {
let (todos, set_todos) = create_signal(Todos(vec![Todo { /* ... */ }]));
// ✅ this has a test associated with it
let num_remaining = move || todos.with(Todos::num_remaining);
}
@@ -53,98 +53,48 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
In general, the less of your logic is wrapped into your components themselves, the
more idiomatic your code will feel and the easier it will be to test.
## 2. Test components with `wasm-bindgen-test`
## 2. Test components with end-to-end (`e2e`) testing
[`wasm-bindgen-test`](https://crates.io/crates/wasm-bindgen-test) is a great utility
for integrating or end-to-end testing WebAssembly apps in a headless browser.
Our [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) directory has several examples with extensive end-to-end testing, using different testing tools.
To use this testing utility, you need to add `wasm-bindgen-test` to your `Cargo.toml`:
The easiest way to see how to use these is to take a look at the test examples themselves:
```toml
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
```
### `wasm-bindgen-test` with [`counter`](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/web.rs)
You should create tests in a separate `tests` directory. You can then run your tests in the browser of your choice:
This is a fairly simple manual testing setup that uses the [`wasm-pack test`](https://rustwasm.github.io/wasm-pack/book/commands/test.html) command.
```bash
wasm-pack test --firefox
```
#### Sample Test
> To see the full setup, check out the tests for the [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) example.
### Writing Your Tests
Most tests will involve some combination of vanilla DOM manipulation and comparison to a `view`. For example, heres a test [for the
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/web.rs).
First, we set up the testing environment.
```rust
use wasm_bindgen_test::*;
use counter::*;
use leptos::*;
use web_sys::HtmlElement;
// tell the test runner to run tests in the browser
wasm_bindgen_test_configure!(run_in_browser);
```
Im going to create a simpler wrapper for each test case, and mount it there.
This makes it easy to encapsulate the test results.
```rust
// like marking a regular test with #[test]
````rust
#[wasm_bindgen_test]
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
document.body().unwrap().append_child(&test_wrapper);
let _ = document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|| view! { <SimpleCounter initial_value=10 step=1/> },
);
}
```
Well use some manual DOM operations to grab the `<div>` that wraps
the whole component, as well as the `clear` button.
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
```rust
// now we extract the buttons by iterating over the DOM
// this would be easier if they had IDs
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = test_wrapper
.query_selector("button")
.unwrap()
.unwrap()
.unchecked_into::<web_sys::HtmlElement>();
```
clear.click();
Now we can use ordinary DOM APIs to simulate user interaction.
```rust
// now let's click the `clear` button
clear.click();
```
You can test individual DOM element attributes or text node values. Sometimes
I like to test the whole view at once. We can do this by testing the elements
`outerHTML` against our expectations.
```rust
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
run_scope(create_runtime(), |cx| {
run_scope(create_runtime(), || {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(cx, 0);
let (value, set_value) = create_signal(0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
@@ -157,24 +107,113 @@ assert_eq!(
.outer_html()
})
);
```
}
````
That test involved us manually replicating the `view` thats inside the component.
There's actually an easier way to do this... We can just test against a `<SimpleCounter/>`
with the initial value `0`. This is where our wrapping element comes in: Ill just test
the wrappers `innerHTML` against another comparison case.
### [`wasm-bindgen-test` with `counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable/tests/web)
This more developed test suite uses a system of fixtures to refactor the manual DOM manipulation of the `counter` tests and easily test a wide range of cases.
#### Sample Test
```rust
assert_eq!(test_wrapper.inner_html(), {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::increment_counter(1);
ui::increment_counter(1);
ui::increment_counter(1);
// Then
assert_eq!(ui::total(), 3);
}
```
### [Playwright with `counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable/e2e)
These tests use the common JavaScript testing tool Playwright to run end-to-end tests on the same example, using a library and testing approach familiar to may who have done frontend development before.
#### Sample Test
```js
import { test, expect } from "@playwright/test";
import { CountersPage } from "./fixtures/counters_page";
test.describe("Increment Count", () => {
test("should increase the total count", async ({ page }) => {
const ui = new CountersPage(page);
await ui.goto();
await ui.addCounter();
await ui.incrementCount();
await ui.incrementCount();
await ui.incrementCount();
await expect(ui.total).toHaveText("3");
});
});
```
This is only a very limited introduction to testing. But I hope its useful as you begin to build applications.
### [Gherkin/Cucumber Tests with `todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/e2e/README.md)
> For more, see [the testing section of the `wasm-bindgen` guide](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/index.html#testing-on-wasm32-unknown-unknown-with-wasm-bindgen-test).
You can integrate any testing tool youd like into this flow. This example uses Cucumber, a testing framework based on natural language.
```
@add_todo
Feature: Add Todo
Background:
Given I see the app
@add_todo-see
Scenario: Should see the todo
Given I set the todo as Buy Bread
When I click the Add button
Then I see the todo named Buy Bread
# @allow.skipped
@add_todo-style
Scenario: Should see the pending todo
When I add a todo as Buy Oranges
Then I see the pending todo
```
The definitions for these actions are defined in Rust code.
```rust
use crate::fixtures::{action, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::{given, when};
#[given("I see the app")]
#[when("I open the app")]
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::goto_path(client, "").await?;
Ok(())
}
#[given(regex = "^I add a todo as (.*)$")]
#[when(regex = "^I add a todo as (.*)$")]
async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::add_todo(client, text.as_str()).await?;
Ok(())
}
// etc.
```
### Learning More
Feel free to check out the CI setup in the Leptos repo to learn more about how to use these tools in your own application. All of these testing methods are run regularly against actual Leptos example apps.

View File

@@ -13,7 +13,7 @@ DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
```rust
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```
@@ -22,10 +22,10 @@ Ill give you the whole thing up front, then walk through it line by line.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! { cx,
view! {
<button
on:click=move |_| {
set_count(3);
@@ -49,18 +49,17 @@ used as a component in your Leptos application. Well see some of the other fe
this macro in a couple chapters.
```rust
fn App(cx: Scope) -> impl IntoView
fn App() -> impl IntoView
```
Every component is a function with the following characteristics
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
as its first argument. This `Scope` is our entrypoint into the reactive system.
By convention, its usually named `cx`.
2. You can include other arguments, which will be available as component “props.”
3. Component functions return `impl IntoView`, which is an opaque type that includes
1. It takes zero or more arguments of any type.
2. It returns `impl IntoView`, which is an opaque type that includes
anything you could return from a Leptos `view`.
> Component function arguments are gathered together into a single props struct which is built by the `view` macro as needed.
## The Component Body
The body of the component function is a set-up function that runs once, not a
@@ -69,7 +68,7 @@ few reactive variables, define any side effects that run in response to those va
changing, and describe the user interface.
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
```
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
@@ -85,7 +84,7 @@ current value, youll call `set_count.set(...)` (or `set_count(...)`).
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
```rust
view! { cx,
view! {
<button
// define an event listener with on:
on:click=move |_| {
@@ -127,7 +126,7 @@ Leptos with `nightly` Rust, signals are already functions, so the closure is unn
As a result, you can write a simpler view:
```rust
view! { cx,
view! {
<button /* ... */>
"Click me: "
// identical to {move || count.get()}
@@ -139,7 +138,7 @@ 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 replace “set this value to 3” with “increment this value by 1”:
@@ -157,9 +156,9 @@ You can see here that while `set_count` just sets the value, `set_count.update()
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
> and docs for whats going on. Feel free to fork the examples to play with them yourself!
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<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>
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -171,16 +170,16 @@ use leptos::*;
// Components are the building blocks of your user interface
// They define a reusable unit of behavior
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> 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);
let (count, set_count) = create_signal(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,
view! {
<button
// on:click will run whenever the `click` event fires
// every event handler is defined as `on:{eventname}`
@@ -221,6 +220,6 @@ fn App(cx: Scope) -> impl IntoView {
// 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/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -12,10 +12,10 @@ increment a counter.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! { cx,
view! {
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
@@ -73,9 +73,9 @@ class=("button-20", move || count() % 2 == 1)
Individual CSS properties can be directly updated with a similar `style:` syntax.
```rust
let (x, set_x) = create_signal(cx, 0);
let (y, set_y) = create_signal(cx, 0);
view! { cx,
let (x, set_x) = create_signal(0);
let (y, set_y) = create_signal(0);
view! {
<div
style="position: absolute"
style:left=move || format!("{}px", x() + 100)
@@ -144,14 +144,32 @@ let double_count = move || count() * 2;
Derived signals let you create reactive computed values that can be used in multiple
places in your application with minimal overhead.
> Note: Using a derived signal like this means that the calculation runs once per
> signal change per place we access `double_count`; in other words, twice. This is a
> very cheap calculation, so thats fine. Well look at memos in a later chapter, which
> are designed to solve this problem for expensive calculations.
Note: Using a derived signal like this means that the calculation runs once per
signal change (when `count()` changes) and once per place we access `double_count`;
in other words, twice. This is a very cheap calculation, so thats fine.
Well look at memos in a later chapter, which re designed to solve this problem
for expensive calculations.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
> #### Advanced Topic: Injecting Raw HTML
>
> The `view` macro provides support for an additional attribute, `inner_html`, which
> can be used to directly set the HTML contents of any element, wiping out any other
> children youve given it. Note that this does _not_ escape the HTML you provide. You
> should make sure that it only contains trusted input or that any HTML entities are
> escaped, to prevent cross-site scripting (XSS) attacks.
>
> ```rust
> let html = "<p>This HTML will be injected.</p>";
> view! {
> <div inner_html=html/>
> }
> ```
>
> [Click here for the full `view` macros docs](https://docs.rs/leptos/latest/leptos/macro.view.html).
<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>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attributes-0-5-lwdrpm?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attributes-0-5-lwdrpm?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>Code Sandbox Source</summary>
@@ -160,15 +178,15 @@ places in your application with minimal overhead.
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(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,
view! {
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
@@ -210,7 +228,33 @@ fn App(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
// 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(App)
}
```

View File

@@ -12,10 +12,10 @@ per click.
You _could_ do this by just creating two `<progress>` elements:
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
let double_count = move || count() * 2;
view! { cx,
view! {
<progress
max="50"
value=count
@@ -35,10 +35,8 @@ Instead, lets create a `<ProgressBar/>` component.
```rust
#[component]
fn ProgressBar(
cx: Scope
) -> impl IntoView {
view! { cx,
fn ProgressBar() -> impl IntoView {
view! {
<progress
max="50"
// hmm... where will we get this from?
@@ -64,10 +62,9 @@ In Leptos, you define props by giving additional arguments to the component func
```rust
#[component]
fn ProgressBar(
cx: Scope,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
view! {
<progress
max="50"
// now this works
@@ -81,9 +78,9 @@ Now we can use our component in the main `<App/>` components view.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! {
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
@@ -118,14 +115,13 @@ argument to the component function with `#[prop(optional)]`.
```rust
#[component]
fn ProgressBar(
cx: Scope,
// mark this prop optional
// you can specify it or not when you use <ProgressBar/>
#[prop(optional)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
view! {
<progress
max=max
value=progress
@@ -149,12 +145,11 @@ with `#[prop(default = ...)`.
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(default = 100)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
view! {
<progress
max=max
value=progress
@@ -171,11 +166,11 @@ as the `progress` prop on another `<ProgressBar/>`.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
let double_count = move || count() * 2;
view! { cx,
view! {
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
@@ -199,7 +194,6 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
```rust
#[component]
fn ProgressBar<F>(
cx: Scope,
#[prop(default = 100)]
max: u16,
progress: F
@@ -207,7 +201,7 @@ fn ProgressBar<F>(
where
F: Fn() -> i32 + 'static,
{
view! { cx,
view! {
<progress
max=max
value=progress
@@ -219,9 +213,24 @@ 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>(
#[prop(default = 100)] max: u16,
progress: F,
) -> impl IntoView {
view! {
<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
@@ -239,14 +248,13 @@ reactive value.
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(default = 100)]
max: u16,
#[prop(into)]
progress: Signal<i32>
) -> impl IntoView
{
view! { cx,
view! {
<progress
max=max
value=progress
@@ -255,22 +263,95 @@ fn ProgressBar(
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
let double_count = move || count() * 2;
view! { cx,
view! {
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
// .into() converts `ReadSignal` to `Signal`
<ProgressBar progress=count/>
// use `Signal::derive()` to wrap a derived signal
<ProgressBar progress=Signal::derive(cx, double_count)/>
<ProgressBar progress=Signal::derive(double_count)/>
}
}
```
### 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>(
#[prop(optional)] progress: Option<F>,
) -> impl IntoView {
progress.map(|progress| {
view! {
<progress
max=100
value=progress
/>
}
})
}
#[component]
pub fn App() -> impl IntoView {
view! {
<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(
#[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>,
) -> impl IntoView {
progress.map(|progress| {
view! {
<progress
max=100
value=progress
/>
}
})
}
#[component]
pub fn App() -> impl IntoView {
view! {
<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.
@@ -285,7 +366,6 @@ component function, and each one of the props:
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
cx: Scope,
/// The maximum value of the progress bar.
#[prop(default = 100)]
max: u16,
@@ -306,9 +386,27 @@ type, and each of the fields used to add props. It can be a little hard to
understand how powerful this is until you hover over the component name or props
and see the power of the `#[component]` macro combined with rust-analyzer here.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
> #### Advanced Topic: `#[component(transparent)]`
>
> All Leptos components return `-> impl IntoView`. Some, though, need to return
> some data directly without any additional wrapping. These can be marked with
> `#[component(transparent)]`, in which case they return exactly the value they
> return, without the rendering system transforming them in any way.
>
> This is mostly used in two situations:
>
> 1. Creating wrappers around `<Suspense/>` or `<Transition/>`, which return a
> transparent suspense structure to integrate with SSR and hydration properly.
> 2. Refactoring `<Route/>` definitions for `leptos_router` out into separate
> components, because `<Route/>` is a transparent component that returns a
> `RouteDefinition` struct rather than a view.
>
> In general, you should not need to use transparent components unless you are
> creating custom wrapping components that fall into one of these two categories.
<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>
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-0-5-5vvl69?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<iframe src="https://codesandbox.io/p/sandbox/3-components-0-5-5vvl69?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -324,8 +422,6 @@ use leptos::*;
/// 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)]
@@ -339,7 +435,7 @@ fn ProgressBar(
/// How much progress should be displayed.
progress: Signal<i32>,
) -> impl IntoView {
view! { cx,
view! {
<progress
max={max}
value=progress
@@ -349,12 +445,12 @@ fn ProgressBar(
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
let double_count = move || count() * 2;
view! { cx,
view! {
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
@@ -372,12 +468,12 @@ fn App(cx: Scope) -> impl IntoView {
<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)/>
<ProgressBar max=50 progress=Signal::derive(double_count)/>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```

View File

@@ -5,7 +5,7 @@ iterating over a list of items is a common task in web applications. Reconciling
the differences between changing sets of items can also be one of the trickiest
tasks for a framework to handle well.
Leptos supports to two different patterns for iterating over items:
Leptos supports two different patterns for iterating over items:
1. For static views: `Vec<_>`
2. For dynamic lists: `<For/>`
@@ -19,30 +19,30 @@ any `Vec<IV> where IV: IntoView` into your view. In other words, if you can rend
```rust
let values = vec![0, 1, 2];
view! { cx,
view! {
// this will just render "012"
<p>{values.clone()}</p>
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { cx, <li>{n}</li>})
.map(|n| view! { <li>{n}</li>})
.collect::<Vec<_>>()}
</ul>
}
```
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
Leptos also provides a `.collect_view()` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
```rust
let values = vec![0, 1, 2];
view! { cx,
view! {
// this will just render "012"
<p>{values.clone()}</p>
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { cx, <li>{n}</li>})
.collect_view(cx)}
.map(|n| view! { <li>{n}</li>})
.collect_view()}
</ul>
}
```
@@ -51,14 +51,15 @@ The fact that the _list_ is static doesnt mean the interface needs to be stat
You can render dynamic items as part of a static list.
```rust
// create a list of N signals
let counters = (1..=length).map(|idx| create_signal(cx, idx));
// create a list of 5 signals
let length = 5;
let counters = (1..=length).map(|idx| create_signal(idx));
// each item manages a reactive view
// but the list itself will never change
let counter_buttons = counters
.map(|(count, set_count)| {
view! { cx,
view! {
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
@@ -68,9 +69,9 @@ let counter_buttons = counters
</li>
}
})
.collect_view(cx);
.collect_view();
view! { cx,
view! {
<ul>{counter_buttons}</ul>
}
```
@@ -86,7 +87,7 @@ keyed dynamic list. It takes three props:
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
- `view`: renders each `T` into a view
- `children`: renders each `T` into a view
`key` is, well, the key. You can add, remove, and move items within the list. As
long as each items key is stable over time, the framework does not need to rerender
@@ -103,9 +104,9 @@ it is generated, and using that as an ID for the key function.
Check out the `<DynamicList/>` component below for an example.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-0-5-pwdn2y?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<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>
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-0-5-pwdn2y?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -120,8 +121,8 @@ use leptos::*;
// 2) for lists that grow, shrink, or move items, using <For/>
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<h1>"Iteration"</h1>
<h2>"Static List"</h2>
<p>"Use this pattern if the list itself is static."</p>
@@ -136,19 +137,18 @@ fn App(cx: Scope) -> impl IntoView {
/// 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));
let counters = (1..=length).map(|idx| create_signal(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,
view! {
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
@@ -163,7 +163,7 @@ fn StaticList(
// 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,
view! {
<ul>{counter_buttons}</ul>
}
}
@@ -172,7 +172,6 @@ fn StaticList(
/// remove counters.
#[component]
fn DynamicList(
cx: Scope,
/// The number of counters to begin with.
initial_length: usize,
) -> impl IntoView {
@@ -190,17 +189,17 @@ fn DynamicList(
// 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)))
.map(|id| (id, create_signal(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 (counters, set_counters) = create_signal(initial_counters);
let add_counter = move |_| {
// create a signal for the new counter
let sig = create_signal(cx, next_counter_id + 1);
let sig = create_signal(next_counter_id + 1);
// add this counter to the list of counters
set_counters.update(move |counters| {
// since `.update()` gives us `&mut T`
@@ -211,7 +210,7 @@ fn DynamicList(
next_counter_id += 1;
};
view! { cx,
view! {
<div>
<button on:click=add_counter>
"Add Counter"
@@ -229,10 +228,10 @@ fn DynamicList(
// 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
// `children` receives each item from your `each` iterator
// and returns a view
view=move |cx, (id, (count, set_count))| {
view! { cx,
children=move |(id, (count, set_count))| {
view! {
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
@@ -258,9 +257,8 @@ fn DynamicList(
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -0,0 +1,278 @@
# Iterating over More Complex Data with `<For/>`
This chapter goes into iteration over nested data structures in a bit
more depth. It belongs here with the other chapter on iteration, but feel
free to skip it and come back if youd like to stick with simpler subjects
for now.
## The Problem
I just said that the framework does not rerender any of the items in one of the
rows, unless the key has changed. This probably makes sense at first, but it can
easily trip you up.
Lets consider an example in which each of the items in our row is some data structure.
Imagine, for example, that the items come from some JSON array of keys and values:
```rust
#[derive(Debug, Clone)]
struct DatabaseEntry {
key: String,
value: i32,
}
```
Lets define a simple component that will iterate over the rows and display each one:
```rust
#[component]
pub fn App() -> impl IntoView {
// start with a set of three rows
let (data, set_data) = create_signal(vec![
DatabaseEntry {
key: "foo".to_string(),
value: 10,
},
DatabaseEntry {
key: "bar".to_string(),
value: 20,
},
DatabaseEntry {
key: "baz".to_string(),
value: 15,
},
]);
view! {
// when we click, update each row,
// doubling its value
<button on:click=move |_| {
set_data.update(|data| {
for row in data {
row.value *= 2;
}
});
// log the new value of the signal
logging::log!("{:?}", data.get());
}>
"Update Values"
</button>
// iterate over the rows and display each value
<For
each=data
key=|state| state.key.clone()
let:child
>
<p>{child.value}</p>
</For>
}
}
```
> Note the `let:child` syntax here. In the previous chapter we introduced `<For/>`
> with a `children` prop. We can actually create this value directly in the children
> of the `<For/>` component, without breaking out of the `view` macro: the `let:child`
> combined with `<p>{child.value}</p>` above is the equivalent of
>
> ```rust
> children=|child| view! { <p>{child.value}</p> }
> ```
When you click the `Update Values` button... nothing happens. Or rather:
the signal is updated, the new value is logged, but the `{child.value}`
for each row doesnt update.
Lets see: is that because we forgot to add a closure to make it reactive?
Lets try `{move || child.value}`.
...Nope. Still nothing.
Heres the problem: as I said, each row is only rerendered when the key changes.
Weve updated the value for each row, but not the key for any of the rows, so
nothing has rerendered. And if you look at the type of `child.value`, its a plain
`i32`, not a reactive `ReadSignal<i32>` or something. This means that even if we
wrap a closure around it, the value in this row will never update.
We have three possible solutions:
1. change the `key` so that it always updates when the data structure changes
2. change the `value` so that its reactive
3. take a reactive slice of the data structure instead of using each row directly
## Option 1: Change the Key
Each row is only rerendered when the key changes. Our rows above didnt rerender,
because the key didnt change. So: why not just force the key to change?
```rust
<For
each=data
key=|state| (state.key.clone(), state.value)
let:child
>
<p>{child.value}</p>
</For>
```
Now we include both the key and the value in the `key`. This means that whenever the
value of a row changes, `<For/>` will treat it as if its an entirely new row, and
replace the previous one.
### Pros
This is very easy. We can make it even easier by deriving `PartialEq`, `Eq`, and `Hash`
on `DatabaseEntry`, in which case we could just `key=|state| state.clone()`.
### Cons
**This is the least efficient of the three options.** Every time the value of a row
changes, it throws out the previous `<p>` element and replaces it with an entirely new
one. Rather than making a fine-grained update to the text node, in other words, it really
does rerender the entire row on every change, and this is expensive in proportion to how
complex the UI of the row is.
Youll notice we also end up cloning the whole data structure so that `<For/>` can hold
onto a copy of the key. For more complex structures, this can become a bad idea fast!
## Option 2: Nested Signals
If we do want that fine-grained reactivity for the value, one option is to wrap the `value`
of each row in a signal.
```rust
#[derive(Debug, Clone)]
struct DatabaseEntry {
key: String,
value: RwSignal<i32>,
}
```
`RwSignal<_>` is a “read-write signal,” which combines the getter and setter in one object.
Im using it here because its a little easier to store in a struct than separate getters
and setters.
```rust
#[component]
pub fn App() -> impl IntoView {
// start with a set of three rows
let (data, set_data) = create_signal(vec![
DatabaseEntry {
key: "foo".to_string(),
value: create_rw_signal(10),
},
DatabaseEntry {
key: "bar".to_string(),
value: create_rw_signal(20),
},
DatabaseEntry {
key: "baz".to_string(),
value: create_rw_signal(15),
},
]);
view! {
// when we click, update each row,
// doubling its value
<button on:click=move |_| {
data.with(|data| {
for row in data {
row.value.update(|value| *value *= 2);
}
});
// log the new value of the signal
logging::log!("{:?}", data.get());
}>
"Update Values"
</button>
// iterate over the rows and display each value
<For
each=data
key=|state| state.key.clone()
let:child
>
<p>{child.value}</p>
</For>
}
}
```
This version works! And if you look in the DOM inspector in your browser, youll
see that unlike in the previous version, in this version only the individual text
nodes are updated. Passing the signal directly into `{child.value}` works, as
signals do keep their reactivity if you pass them into the view.
Note that I changed the `set_data.update()` to a `data.with()`. `.with()` is the
non-cloning way of accessing a signals value. In this case, we are only updating
the internal values, not updating the list of values: because signals maintain their
own state, we dont actual need to update the `data` signal at all, so the immutable
`.with()` is fine here.
> In fact, this version doesnt update `data`, so the `<For/>` is essentially a static
> list as in the last chapter, and this could just be a plain iterator. But the `<For/>`
> is useful if we want to add or remove rows in the future.
### Pros
This is the most efficient option, and fits directly with the rest of the mental model
of the framework: values that change over time are wrapped in signals so the interface
can respond to them.
### Cons
Nested reactivity can be cumbersome if youre receiving data from an API or another
data source you dont control, and you dont want to create a different struct wrapping
each field in a signal.
## Option 3: Memoized Slices
Leptos provides a primitive called [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html),
which creates a derived computation that only triggers a reactive update when its value
has changed.
This allows you to create reactive values for subfields of a larger data structure,
without needing to wrap the fields of that structure in signals.
Most of the application can remain the same as the initial (broken) version, but the `<For/>`
will be updated to this:
```rust
<For
each=move || data().into_iter().enumerate()
key=|(_, state)| state.key.clone()
children=move |(index, _)| {
let value = create_memo(move |_| {
data.with(|data| data.get(index).map(|d| d.value).unwrap_or(0))
});
view! {
<p>{value}</p>
}
}
/>
```
Youll notice a few differences here:
- we convert the `data` signal into an enumerated iterator
- we use the `children` prop explicitly, to make it easier to run some non-`view` code
- we define a `value` memo and use that in the view. This `value` field doesnt actually
use the `child` being passed into each row. Instead, it uses the index and reaches back
into the original `data` to get the value.
Every time `data` changes, now, each memo will be recalculated. If its value has changed,
it will update its text node, without rerendering the whole row.
## Pros
We get the same fine-grained reactivity of the signal-wrapped version, without needing to
wrap the data in signals.
## Cons
Its a bit more complex to set up this memo-per-row inside the `<For/>` loop rather than
using nested signals. For example, youll notice that we have to guard against the possibility
that the `data[index]` would panic by using `data.get(index)`, because this memo may be
triggered to re-run once just after the row is removed. (This is because the memo for each row
and the whole `<For/>` both depend on the same `data` signal, and the order of execution for
multiple reactive values that depend on the same signal isnt guaranteed.)
Note also that while memos memoize their reactive changes, the same
calculation does need to re-run to check the value every time, so nested reactive signals
will still be more efficient for pinpoint updates here.

View File

@@ -19,12 +19,13 @@ There are two important things to remember:
2. The `value` _attribute_ only sets the initial value of the input, i.e., it
only updates the input up to the point that you begin typing. The `value`
_property_ continues updating the input after that. You usually want to set
`prop:value` for this reason.
`prop:value` for this reason. (The same is true for `checked` and `prop:checked`
on an `<input type="checkbox">`.)
```rust
let (name, set_name) = create_signal(cx, "Controlled".to_string());
let (name, set_name) = create_signal("Controlled".to_string());
view! { cx,
view! {
<input type="text"
on:input=move |ev| {
// event_target_value is a Leptos helper function
@@ -42,6 +43,33 @@ view! { cx,
}
```
> #### Why do you need `prop:value`?
>
> Web browsers are the most ubiquitous and stable platform for rendering graphical user interfaces in existence. They have also maintained an incredible backwards compatibility over their three decades of existence. Inevitably, this means there are some quirks.
>
> One odd quirk is that there is a distinction between HTML attributes and DOM element properties, i.e., between something called an “attribute” which is parsed from HTML and can be set on a DOM element with `.setAttribute()`, and something called a “property” which is a field of the JavaScript class representation of that parsed HTML element.
>
> In the case of an `<input value=...>`, setting the `value` _attribute_ is defined as setting the initial value for the input, and setting `value` _property_ sets its current value. It maybe easiest to understand this by opening `about:blank` and running the following JavaScript in the browser console, line by line:
>
> ```js
> // create an input and append it to the DOM
> const el = document.createElement("input");
> document.body.appendChild(el);
>
> el.setAttribute("value", "test"); // updates the input
> el.setAttribute("value", "another test"); // updates the input again
>
> // now go and type into the input: delete some characters, etc.
>
> el.setAttribute("value", "one more time?");
> // nothing should have changed. setting the "initial value" does nothing now
>
> // however...
> el.value = "But this works";
> ```
>
> Many other frontend frameworks conflate attributes and properties, or create a special case for inputs that sets the value correctly. Maybe Leptos should do this too; but for now, I prefer giving users the maximum amount of control over whether theyre setting an attribute or a property, and doing my best to educate people about the actual underlying browser behavior rather than obscuring it.
## Uncontrolled Inputs
In an "uncontrolled input," the browser controls the state of the input element.
@@ -53,9 +81,9 @@ In this example, we only notify the framework when the `<form>` fires a `submit`
event.
```rust
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
let (name, set_name) = create_signal("Uncontrolled".to_string());
let input_element: NodeRef<Input> = create_node_ref(cx);
let input_element: NodeRef<Input> = create_node_ref();
```
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
@@ -89,7 +117,7 @@ We can then call `.value()` to get the value out of the input, because `NodeRef`
gives us access to a correctly-typed HTML element.
```rust
view! { cx,
view! {
<form on:submit=on_submit>
<input type="text"
value=name
@@ -109,9 +137,9 @@ The view should be pretty self-explanatory by now. Note two things:
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
They are the same thing, but `node_ref` has better rust-analyzer support.)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-forms-0-5-rf2t7c?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<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>
<iframe src="https://codesandbox.io/p/sandbox/5-forms-0-5-rf2t7c?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -120,8 +148,8 @@ The view should be pretty self-explanatory by now. Note two things:
use leptos::{ev::SubmitEvent, *};
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<h2>"Controlled Component"</h2>
<ControlledComponent/>
<h2>"Uncontrolled Component"</h2>
@@ -130,11 +158,11 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
fn ControlledComponent(cx: Scope) -> impl IntoView {
fn ControlledComponent() -> impl IntoView {
// create a signal to hold the value
let (name, set_name) = create_signal(cx, "Controlled".to_string());
let (name, set_name) = create_signal("Controlled".to_string());
view! { cx,
view! {
<input type="text"
// fire an event whenever the input changes
on:input=move |ev| {
@@ -164,15 +192,15 @@ fn ControlledComponent(cx: Scope) -> impl IntoView {
}
#[component]
fn UncontrolledComponent(cx: Scope) -> impl IntoView {
fn UncontrolledComponent() -> impl IntoView {
// import the type for <input>
use leptos::html::Input;
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
let (name, set_name) = create_signal("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);
let input_element: NodeRef<Input> = create_node_ref();
// fires when the form `submit` event happens
// this will store the value of the <input> in our signal
@@ -192,7 +220,7 @@ fn UncontrolledComponent(cx: Scope) -> impl IntoView {
set_name(value);
};
view! { cx,
view! {
<form on:submit=on_submit>
<input type="text"
// here, we use the `value` *attribute* to set only
@@ -214,9 +242,8 @@ fn UncontrolledComponent(cx: Scope) -> impl IntoView {
// 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/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -38,7 +38,7 @@ special knowledge.
For example, lets start with a simple signal and derived signal:
```rust
let (value, set_value) = create_signal(cx, 0);
let (value, set_value) = create_signal(0);
let is_odd = move || value() & 1 == 1;
```
@@ -54,7 +54,7 @@ Lets say I want to render some text if the number is odd, and some other text
if its even. Well, how about this?
```rust
view! { cx,
view! {
<p>
{move || if is_odd() {
"Odd"
@@ -81,7 +81,7 @@ let message = move || {
}
};
view! { cx,
view! {
<p>{message}</p>
}
```
@@ -90,7 +90,7 @@ This works fine. We can make it a little shorter if wed like, using `bool::th
```rust
let message = move || is_odd().then(|| "Ding ding ding!");
view! { cx,
view! {
<p>{message}</p>
}
```
@@ -112,7 +112,7 @@ let message = move || {
_ => "Even"
}
};
view! { cx,
view! {
<p>{message}</p>
}
```
@@ -131,7 +131,7 @@ above, where the value switches from even to odd on every change, this is fine.
But consider the following example:
```rust
let (value, set_value) = create_signal(cx, 0);
let (value, set_value) = create_signal(0);
let message = move || if value() > 5 {
"Big"
@@ -139,7 +139,7 @@ let message = move || if value() > 5 {
"Small"
};
view! { cx,
view! {
<p>{message}</p>
}
```
@@ -148,10 +148,10 @@ This _works_, for sure. But if you added a log, you might be surprised
```rust
let message = move || if value() > 5 {
log!("{}: rendering Big", value());
logging::log!("{}: rendering Big", value());
"Big"
} else {
log!("{}: rendering Small", value());
logging::log!("{}: rendering Small", value());
"Small"
};
```
@@ -194,12 +194,12 @@ the answer. You pass it a `when` condition function, a `fallback` to be shown if
the `when` function returns `false`, and children to be rendered if `when` is `true`.
```rust
let (value, set_value) = create_signal(cx, 0);
let (value, set_value) = create_signal(0);
view! { cx,
view! {
<Show
when=move || { value() > 5 }
fallback=|cx| view! { cx, <Small/> }
fallback=|| view! { <Small/> }
>
<Big/>
</Show>
@@ -208,7 +208,8 @@ view! { cx,
`<Show/>` memoizes the `when` condition, so it only renders its `<Small/>` once,
continuing to show the same component until `value` is greater than five;
then it renders `<Big/>` once, continuing to show it indefinitely.
then it renders `<Big/>` once, continuing to show it indefinitely or until `value`
goes below five and then renders `<Small/>` again.
This is a helpful tool to avoid rerendering when using dynamic `if` expressions.
As always, there's some overhead: for a very simple node (like updating a single
@@ -227,19 +228,19 @@ can be a little annoying if youre returning different HTML elements from
different branches of a conditional:
```rust,compile_error
view! { cx,
view! {
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }
view! { <pre>"One"</pre> }
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }
view! { <p>"Two"</p> }
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }
_ => view! { <textarea>{value()}</textarea> }
}}
</main>
}
@@ -259,32 +260,32 @@ to get yourself out of this situation:
1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>`
with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any)
2. If you have a variety of view types that are not all `HtmlElement`, convert them to
`View`s with [`.into_view(cx)`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
`View`s with [`.into_view()`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
Heres the same example, with the conversion added:
```rust,compile_error
view! { cx,
view! {
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }.into_any()
view! { <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }.into_any()
view! { <p>"Two"</p> }.into_any()
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
_ => view! { <textarea>{value()}</textarea> }.into_any()
}}
</main>
}
```
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<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>
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-0-5-4yn7qz?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -293,12 +294,12 @@ view! { cx,
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (value, set_value) = create_signal(0);
let is_odd = move || value() & 1 == 1;
let odd_text = move || if is_odd() { Some("How odd!") } else { None };
view! { cx,
view! {
<h1>"Control Flow"</h1>
// Simple UI to update and show a value
@@ -345,39 +346,38 @@ fn App(cx: Scope) -> impl IntoView {
// 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> }
fallback=|| view! { <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> })}
{move || is_odd().then(|| view! { <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)
// or `.into_view()` (for all view types)
{move || match is_odd() {
true if value() == 1 => {
// <pre> returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }.into_any()
view! { <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! { <p>"Two"</p> }.into_any()
}
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
_ => view! { <textarea>{value()}</textarea> }.into_any()
}}
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -10,16 +10,16 @@ Lets start with a simple component to capture a number input.
```rust
#[component]
fn NumericInput(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
fn NumericInput() -> impl IntoView {
let (value, set_value) = create_signal(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,
view! {
<label>
"Type a number (or not!)"
<input on:input=on_input/>
<input type="number" on:input=on_input/>
<p>
"You entered "
<strong>{value}</strong>
@@ -60,27 +60,27 @@ Lets add an `<ErrorBoundary/>` to this example.
```rust
#[component]
fn NumericInput(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
fn NumericInput() -> impl IntoView {
let (value, set_value) = create_signal(Ok(0));
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
view! {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input on:input=on_input/>
<input type="number" on:input=on_input/>
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
fallback=|errors| view! {
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors as strings, if we'd like
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect_view(cx)
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
.collect_view()
}
</ul>
</div>
@@ -110,9 +110,9 @@ Not a number! Errors:
If you fix the error, the error message will disappear and the content youre wrapping in
an `<ErrorBoundary/>` will appear again.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-errors-0-5-5mptv9?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<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>
<iframe src="https://codesandbox.io/p/sandbox/7-errors-0-5-5mptv9?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -121,13 +121,13 @@ an `<ErrorBoundary/>` will appear again.
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
fn App() -> impl IntoView {
let (value, set_value) = create_signal(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,
view! {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
@@ -137,7 +137,7 @@ fn App(cx: Scope) -> impl IntoView {
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
fallback=|errors| view! {
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
@@ -145,7 +145,7 @@ fn App(cx: Scope) -> impl IntoView {
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
}
</ul>
@@ -167,9 +167,8 @@ fn App(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -29,17 +29,17 @@ it in the child. This lets you manipulate the state of the parent from the child
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<ButtonA setter=set_toggled/>
}
}
#[component]
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> impl IntoView {
view! { cx,
pub fn ButtonA(setter: WriteSignal<bool>) -> impl IntoView {
view! {
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -62,9 +62,9 @@ Another approach would be to pass a callback to the child: say, `on_click`.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
}
@@ -72,14 +72,9 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ButtonB<F>(
cx: Scope,
on_click: F,
) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
pub fn ButtonB(#[prop(into)] on_click: Callback<MouseEvent>) -> impl IntoView
{
view! { cx,
view! {
<button on:click=on_click>
"Toggle"
</button>
@@ -93,10 +88,50 @@ of keeping local state local, preventing the problem of spaghetti mutation. But
the logic to mutate that signal needs to exist up in `<App/>`, not down in `<ButtonB/>`. These
are real trade-offs, not a simple right-or-wrong choice.
> Note the way we use the `Callback<In, Out>` type. This is basically a
> wrapper around a closure `Fn(In) -> Out` that is also `Copy` and makes it
> easy to pass around.
>
> We also used the `#[prop(into)]` attribute so we can pass a normal closure into
> `on_click`. Please see the [chapter "`into` Props"](./03_components.md#into-props) for more details.
### 2.1 Use Closure instead of `Callback`
You can use a Rust closure `Fn(MouseEvent)` directly instead of `Callback`:
```rust
#[component]
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
}
}
#[component]
pub fn ButtonB<F>(on_click: F) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
pub fn ButtonB(#[prop(into)] on_click: Callback<MouseEvent>) -> impl IntoView
{
view! {
<button on:click=on_click>
"Toggle"
</button>
}
}
```
The code is very similar in this case. On more advanced use-cases using a
closure might require some cloning compared to using a `Callback`.
> Note the way we declare the generic type `F` here for the callback. If youre
> confused, look back at the [generic props](./03_components.html#generic-props) section
> of the chapter on components.
## 3. Use an Event Listener
You can actually write Option 2 in a slightly different way. If the callback maps directly onto
@@ -105,9 +140,9 @@ in your `view` macro in `<App/>`.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
// note the on:click instead of on_click
// this is the same syntax as an HTML element event listener
@@ -117,8 +152,8 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
view! { cx,
pub fn ButtonC<F>() -> impl IntoView {
view! {
<button>"Toggle"</button>
}
}
@@ -141,17 +176,17 @@ tree:
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<Layout/>
}
}
#[component]
pub fn Layout(cx: Scope) -> impl IntoView {
view! { cx,
pub fn Layout() -> impl IntoView {
view! {
<header>
<h1>"My Page"</h1>
</header>
@@ -162,8 +197,8 @@ pub fn Layout(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Content(cx: Scope) -> impl IntoView {
view! { cx,
pub fn Content() -> impl IntoView {
view! {
<div class="content">
<ButtonD/>
</div>
@@ -171,7 +206,7 @@ pub fn Content(cx: Scope) -> impl IntoView {
}
#[component]
pub fn ButtonD<F>(cx: Scope) -> impl IntoView {
pub fn ButtonD<F>() -> impl IntoView {
todo!()
}
```
@@ -182,17 +217,17 @@ pass your `WriteSignal` to its props. You could do whats sometimes called
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<Layout set_toggled/>
}
}
#[component]
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
view! { cx,
pub fn Layout(set_toggled: WriteSignal<bool>) -> impl IntoView {
view! {
<header>
<h1>"My Page"</h1>
</header>
@@ -203,8 +238,8 @@ pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
}
#[component]
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
view! { cx,
pub fn Content(set_toggled: WriteSignal<bool>) -> impl IntoView {
view! {
<div class="content">
<ButtonD set_toggled/>
</div>
@@ -212,7 +247,7 @@ pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
}
#[component]
pub fn ButtonD<F>(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
pub fn ButtonD<F>(set_toggled: WriteSignal<bool>) -> impl IntoView {
todo!()
}
```
@@ -237,13 +272,13 @@ unnecessary prop drilling.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
// share `set_toggled` with all children of this component
provide_context(cx, set_toggled);
provide_context(set_toggled);
view! { cx,
view! {
<p>"Toggled? " {toggled}</p>
<Layout/>
}
@@ -252,14 +287,14 @@ pub fn App(cx: Scope) -> impl IntoView {
// <Layout/> and <Content/> omitted
#[component]
pub fn ButtonD(cx: Scope) -> impl IntoView {
pub fn ButtonD() -> impl IntoView {
// use_context searches up the context tree, hoping to
// find a `WriteSignal<bool>`
// in this case, I .expect() because I know I provided it
let setter = use_context::<WriteSignal<bool>>(cx)
let setter = use_context::<WriteSignal<bool>>()
.expect("to have found the setter provided");
view! { cx,
view! {
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -285,9 +320,9 @@ in `<ButtonD/>` and a single text node in `<App/>`. Its as if the components
themselves dont exist at all. And, well... at runtime, they dont. Its just
signals and effects, all the way down.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-0-5-7rz7qd?file=%2Fsrc%2Fmain.rs%3A1%2C2)
<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>
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-0-5-7rz7qd?file=%2Fsrc%2Fmain.rs%3A1%2C2" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -308,20 +343,19 @@ use leptos::{ev::MouseEvent, *};
struct SmallcapsContext(WriteSignal<bool>);
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> 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);
let (red, set_red) = create_signal(false);
let (right, set_right) = create_signal(false);
let (italics, set_italics) = create_signal(false);
let (smallcaps, set_smallcaps) = create_signal(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));
provide_context(SmallcapsContext(set_smallcaps));
view! {
cx,
<main>
<p
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
@@ -353,12 +387,10 @@ pub fn App(cx: Scope) -> impl IntoView {
/// 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)
>
@@ -370,7 +402,6 @@ pub fn ButtonA(
/// 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
@@ -378,7 +409,6 @@ where
F: Fn(MouseEvent) + 'static,
{
view! {
cx,
<button
on:click=on_click
>
@@ -402,9 +432,8 @@ where
/// 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 {
pub fn ButtonC() -> impl IntoView {
view! {
cx,
<button>
"Toggle Italics"
</button>
@@ -414,11 +443,10 @@ pub fn ButtonC(cx: Scope) -> impl IntoView {
/// 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;
pub fn ButtonD() -> impl IntoView {
let setter = use_context::<SmallcapsContext>().unwrap().0;
view! {
cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -428,9 +456,8 @@ pub fn ButtonD(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -5,7 +5,7 @@ children into an HTML element. For example, imagine I have a `<FancyForm/>` comp
that enhances an HTML `<form>`. I need some way to pass all its inputs.
```rust
view! { cx,
view! {
<Form>
<fieldset>
<label>
@@ -28,12 +28,12 @@ other components:
In fact, youve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
```rust
view! { cx,
view! {
<Show
// `when` is a normal prop
when=move || value() > 5
// `fallback` is a "render prop": a function that returns a view
fallback=|cx| view! { cx, <Small/> }
fallback=|| view! { <Small/> }
>
// `<Big/>` (and anything else here)
// will be given to the `children` prop
@@ -47,7 +47,6 @@ Lets define a component that takes some children and a render prop.
```rust
#[component]
pub fn TakesChildren<F, IV>(
cx: Scope,
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,
@@ -58,19 +57,19 @@ where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
view! {
<h2>"Render Prop"</h2>
{render_prop()}
<h2>"Children"</h2>
{children(cx)}
{children()}
}
}
```
`render_prop` and `children` are both functions, so we can call them to generate
the appropriate views. `children`, in particular, is an alias for
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
`Box<dyn FnOnce() -> Fragment>`. (Aren't you glad we named it `Children` instead?)
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
@@ -78,8 +77,8 @@ the appropriate views. `children`, in particular, is an alias for
We can use the component like this:
```rust
view! { cx,
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
view! {
<TakesChildren render_prop=|| view! { <p>"Hi, there!"</p> }>
// these get passed to `children`
"Some text"
<span>"A span"</span>
@@ -97,15 +96,15 @@ a component that takes its children and turns them into an unordered list.
```rust
#[component]
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
pub fn WrapsChildren(children: Children) -> impl IntoView {
// Fragment has `nodes` field that contains a Vec<View>
let children = children(cx)
let children = children()
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect_view(cx);
.map(|child| view! { <li>{child}</li> })
.collect_view();
view! { cx,
view! {
<ul>{children}</ul>
}
}
@@ -114,18 +113,18 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
Calling it like this will create a list:
```rust
view! { cx,
<WrappedChildren>
view! {
<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)
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-0-5-m4jwhp?file=%2Fsrc%2Fmain.rs%3A1%2C1)
<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>
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-0-5-m4jwhp?file=%2Fsrc%2Fmain.rs%3A1%2C1" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -142,19 +141,19 @@ use leptos::*;
// property
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (items, set_items) = create_signal(cx, vec![0, 1, 2]);
pub fn App() -> impl IntoView {
let (items, set_items) = create_signal(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,
view! {
<p>"Length: " {len}</p>
}
};
view! { cx,
view! {
// This component just displays the two kinds of children,
// embedding them in some other markup
<TakesChildren
@@ -179,12 +178,11 @@ pub fn App(cx: Scope) -> impl IntoView {
/// 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>`
/// this is an alias for `Box<dyn FnOnce() -> Fragment>`
/// ... aren't you glad we named it `Children` instead?
children: Children,
) -> impl IntoView
@@ -192,30 +190,30 @@ where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
view! {
<h1><code>"<TakesChildren/>"</code></h1>
<h2>"Render Prop"</h2>
{render_prop()}
<hr/>
<h2>"Children"</h2>
{children(cx)}
{children()}
}
}
/// 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
pub fn WrapsChildren(children: Children) -> impl IntoView {
// children() 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)
let children = children()
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.map(|child| view! { <li>{child}</li> })
.collect::<Vec<_>>();
view! { cx,
view! {
<h1><code>"<WrapsChildren/>"</code></h1>
// wrap our wrapped children in a UL
<ul>{children}</ul>
@@ -223,9 +221,8 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -0,0 +1,98 @@
# No Macros: The View Builder Syntax
> If youre perfectly happy with the `view!` macro syntax described so far, youre welcome to skip this chapter. The builder syntax described in this section is always available, but never required.
For one reason or another, many developers would prefer to avoid macros. Perhaps you dont like the limited `rustfmt` support. (Although, you should check out [`leptosfmt`](https://github.com/bram209/leptosfmt), which is an excellent tool!) Perhaps you worry about the effect of macros on compile time. Perhaps you prefer the aesthetics of pure Rust syntax, or you have trouble context-switching between an HTML-like syntax and your Rust code. Or perhaps you want more flexibility in how you create and manipulate HTML elements than the `view` macro provides.
If you fall into any of those camps, the builder syntax may be for you.
The `view` macro expands an HTML-like syntax to a series of Rust functions and method calls. If youd rather not use the `view` macro, you can simply use that expanded syntax yourself. And its actually pretty nice!
First off, if you want you can even drop the `#[component]` macro: a component is just a setup function that creates your view, so you can define a component as a simple function call:
```rust
pub fn counter(initial_value: i32, step: u32) -> impl IntoView { }
```
Elements are created by calling a function with the same name as the HTML element:
```rust
p()
```
You can add children to the element with [`.child()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child), which takes a single child or a tuple or array of types that implement [`IntoView`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
```rust
p().child((em().child("Big, "), strong().child("bold "), "text"))
```
Attributes are added with [`.attr()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.attr). This can take any of the same types that you could pass as an attribute into the view macro (types that implement [`IntoAttribute`](https://docs.rs/leptos/latest/leptos/trait.IntoAttribute.html)).
```rust
p().attr("id", "foo").attr("data-count", move || count().to_string())
```
Similarly, the `class:`, `prop:`, and `style:` syntaxes map directly onto [`.class()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.class), [`.prop()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.prop), and [`.style()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.style) methods.
Event listeners can be added with [`.on()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.on). Typed events found in [`leptos::ev`](https://docs.rs/leptos/latest/leptos/ev/index.html) prevent typos in event names and allow for correct type inference in the callback function.
```rust
button()
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.child("Clear")
```
> Many additional methods can be found in the [`HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child) docs, including some methods that are not directly available in the `view` macro.
All of this adds up to a very Rusty syntax to build full-featured views, if you prefer this style.
```rust
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(0);
div()
.child((
button()
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.child("Clear"),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
})
.child("-1"),
span().child(("Value: ", move || count.get().value(), "!")),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
})
.child("+1"),
))
}
```
This also has the benefit of being more flexible: because these are all plain Rust functions and methods, its easier to use them in things like iterator adapters without any additional “magic”:
```rust
// take some set of attribute names and values
let attrs: Vec<(&str, AttributeValue)> = todo!();
// you can use the builder syntax to “spread” these onto the
// element in a way thats not possible with the view macro
let p = attrs
.into_iter()
.fold(p(), |el, (name, value)| el.attr(name, value));
```
> ## Performance Note
>
> One caveat: the `view` macro applies significant optimizations in server-side-rendering (SSR) mode to improve HTML rendering performance significantly (think 2-4x faster, depending on the characteristics of any given app). It does this by analyzing your `view` at compile time and converting the static parts into simple HTML strings, rather than expanding them into the builder syntax.
>
> This means two things:
>
> 1. The builder syntax and `view` macro should not be mixed, or should only be mixed very carefully: at least in SSR mode, the output of the `view` should be treated as a “black box” that cant have additional builder methods applied to it without causing inconsistencies.
> 2. Using the builder syntax will result in less-than-optimal SSR performance. It wont be slow, by any means (and its worth running your own benchmarks in any case), just slower than the `view`-optimized version.

View File

@@ -5,32 +5,35 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
"counters",
"counters_stable",
"counter_without_macros",
"error_boundary",
"errors_axum",
"fetch",
"hackernews",
"hackernews_axum",
"js-framework-benchmark",
"leptos-tailwind-axum",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"slots",
"ssr_modes",
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
"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",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"slots",
"ssr_modes",
"ssr_modes_axum",
"suspense_tests",
"tailwind_actix",
"tailwind_csr",
"tailwind_axum",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]
[tasks.gen-members]
@@ -38,10 +41,113 @@ workspace = false
description = "Generate the list of workspace members"
script = '''
examples=$(ls |
grep -v README.md |
grep -v .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.test-report]
workspace = false
description = "report web testing technology used by examples - OPTION: [all]"
script = '''
set -emu
BOLD="\e[1m"
GREEN="\e[0;32m"
ITALIC="\e[3m"
YELLOW="\e[0;33m"
RESET="\e[0m"
echo
echo "${YELLOW}Web Test Technology${RESET}"
echo
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
sed 's%./%%' |
sed 's%/Makefile.toml%%' |
grep -v Makefile.toml |
sort -u)
start_path=$(pwd)
for path in $makefile_paths; do
cd $path
crate_symbols=
pw_count=$(find . -name playwright.config.ts | wc -l)
while read -r line; do
case $line in
*"cucumber"*)
crate_symbols=$crate_symbols"C"
;;
*"fantoccini"*)
crate_symbols=$crate_symbols"D"
;;
esac
done <"./Cargo.toml"
while read -r line; do
case $line in
*"cargo-make/wasm-test.toml"*)
crate_symbols=$crate_symbols"W"
;;
*"cargo-make/playwright-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"N"
;;
*"cargo-make/playwright-trunk-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/trunk_server.toml"*)
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
crate_symbols=$crate_symbols"L"
;;
*"cargo-make/cargo-leptos-test.toml"*)
crate_symbols=$crate_symbols"L"
if [ $pw_count -gt 0 ]; then
crate_symbols=$crate_symbols"P"
fi
;;
esac
done <"./Makefile.toml"
# Sort list of tools
sorted_crate_symbols=$(echo ${crate_symbols} | grep -o . | sort | tr -d "\n")
formatted_crate_symbols="${BOLD}${YELLOW}${sorted_crate_symbols}${RESET}"
crate_line=$path
if [ ! -z ${1+x} ]; then
# Show all examples
if [ ! -z $crate_symbols ]; then
crate_line=$crate_line$formatted_crate_symbols
fi
echo $crate_line
elif [ ! -z $crate_symbols ]; then
# Filter out examples that do not run tests in `ci`
crate_line=$crate_line$formatted_crate_symbols
echo $crate_line
fi
cd ${start_path}
done
c="${BOLD}${YELLOW}C${RESET} = Cucumber"
d="${BOLD}${YELLOW}D${RESET} = WebDriver"
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
n="${BOLD}${YELLOW}N${RESET} = Node"
p="${BOLD}${YELLOW}P${RESET} = Playwright"
t="${BOLD}${YELLOW}T${RESET} = Trunk"
w="${BOLD}${YELLOW}W${RESET} = WASM"
echo
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
echo
'''

View File

@@ -1,7 +1,45 @@
# Examples
# Examples README
## Main Branch
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 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 but 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).
To see the examples as they were at the time of the `0.5.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.5.0/examples).
## Cargo Make
[Cargo Make](https://sagiegurari.github.io/cargo-make/) is used to build, test, and run examples.
Here are the highlights.
- Extendable custom task files are located in the [cargo-make](./cargo-make/) directory
- Running a task will automatically install `cargo` dependencies
- Each `Makefile.toml` file must extend the [cargo-make/main.toml](./cargo-make/main.toml) file
- [cargo-make](./cargo-make/) files that end in `*-test.toml` configure web testing strategies
- Run `cargo make test-report` to learn which examples have web tests
## Getting Started
Follow these steps to get any example up and running.
1. `cd` to the example root directory
2. Run `cargo make ci` to setup and test the example
3. Run `cargo make start` to run the example
4. Open the client URL in the console output (<http://127.0.0.1:8080> or <http://127.0.0.1:3000> by default)
## Prerequisites
Example projects depend on the following tools. Please install them as needed.
- [Rust](https://www.rust-lang.org/)
- Nightly Rust
- Run `rustup toolchain install nightly`
- Run `rustup target add wasm32-unknown-unknown`
- [Cargo Make](https://sagiegurari.github.io/cargo-make/)
- Run `cargo install --force cargo-make`
- Setup a command alias like `alias cm='cargo make'` to reduce typing (**_Optional_**)
- [Node Version Manager](https://github.com/nvm-sh/nvm/) (**_Optional_**)
- [Node.js](https://nodejs.org/)
- [pnpm](https://pnpm.io/) (**_Optional_**)

68
examples/SSR_NOTES.md Normal file
View File

@@ -0,0 +1,68 @@
# Server Side Rendering
## Cargo Leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## WASM Pack
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. For examples with CSS you also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```
### Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
to generate the WebAssembly to hydrate the HTML delivered from the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above before running
> `cargo run`

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,4 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]

View File

@@ -0,0 +1,10 @@
# Animated Show Example
This is a very simple example of the `<AnimatedShow>` component.
The `<AnimatedShow>` 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.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.

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>

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,34 @@
use core::time::Duration;
use leptos::*;
#[component]
pub fn App() -> impl IntoView {
let show = create_rw_signal(false);
// the CSS classes in this example are just written directly inside the `index.html`
view! {
<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,8 @@
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(App);
}

View File

@@ -1,6 +1,4 @@
[tasks.integration-test]
dependencies = ["cargo-leptos-e2e"]
extend = { path = "./cargo-leptos.toml" }
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.integration-test]
dependencies = ["install-cargo-leptos", "cargo-leptos-e2e"]

View File

@@ -0,0 +1,7 @@
extend = [
{ path = "./cargo-leptos.toml" },
{ path = "../cargo-make/webdriver.toml" },
]
[tasks.integration-test]
dependencies = ["install-cargo-leptos", "start-webdriver", "cargo-leptos-e2e"]

View File

@@ -0,0 +1,32 @@
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.build]
clear = true
command = "cargo"
args = ["leptos", "build"]
[tasks.check]
clear = true
dependencies = ["check-debug", "check-release"]
[tasks.check-debug]
toolchain = "nightly"
command = "cargo"
args = ["check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-release]
toolchain = "nightly"
command = "cargo"
args = ["check-all-features", "--release"]
install_crate = "cargo-all-features"
[tasks.start-client]
dependencies = ["install-cargo-leptos"]
command = "cargo"
args = ["leptos", "watch"]

View File

@@ -1,18 +1,19 @@
[tasks.clean]
dependencies = [
"clean-cargo",
"clean-trunk",
"clean-node_modules",
"clean-playwright",
"clean-cargo",
"clean-trunk",
"clean-node_modules",
"clean-playwright",
]
[tasks.clean-cargo]
command = "cargo"
args = ["clean"]
command = "rm"
args = ["-rf", "target"]
[tasks.clean-trunk]
command = "trunk"
args = ["clean"]
script = '''
find . -type d -name dist | xargs rm -rf
'''
[tasks.clean-node_modules]
script = '''

View File

@@ -0,0 +1,34 @@
[tasks.start-client]
[tasks.stop-client]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ ! -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
pkill -ef ${CLIENT_PROCESS_NAME}
fi
'''
[tasks.client-status]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
echo " ${CLIENT_PROCESS_NAME} is not running"
else
echo " ${CLIENT_PROCESS_NAME} is up"
fi
'''
[tasks.maybe-start-client]
condition = { env_set = ["CLIENT_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${CLIENT_PROCESS_NAME}) ]; then
echo " Starting ${CLIENT_PROCESS_NAME}"
if [ -z ${SPAWN_CLIENT_PROCESS} ];then
cargo make start-client ${@} &
else
cargo make start-client ${@}
fi
else
echo " ${CLIENT_PROCESS_NAME} is already started"
fi
'''

View File

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

View File

@@ -1,7 +1,9 @@
extend = [
{ path = "../cargo-make/clean.toml" },
{ path = "../cargo-make/lint.toml" },
{ path = "../cargo-make/node.toml" },
{ path = "./compile.toml" },
{ path = "./clean.toml" },
{ path = "./lint.toml" },
{ path = "./node.toml" },
{ path = "./process.toml" },
]
# CI Stages
@@ -9,9 +11,6 @@ extend = [
[tasks.ci]
dependencies = ["prepare", "lint", "build", "test-flow", "integration-test"]
[tasks.ci-clean]
dependencies = ["ci", "clean"]
[tasks.prepare]
dependencies = ["setup-node"]
@@ -20,6 +19,17 @@ dependencies = ["check-style"]
[tasks.integration-test]
# Support Local Runs
[tasks.ci-clean]
dependencies = ["ci", "clean"]
[tasks.check-clean]
dependencies = ["check", "clean"]
[tasks.build-clean]
dependencies = ["build", "clean"]
# ALIASES
[tasks.verify-flow]

View File

@@ -0,0 +1,14 @@
extend = [
{ path = "../cargo-make/playwright.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]
[tasks.integration-test]
description = "Run integration test with automated start and stop of processes"
env = { SPAWN_CLIENT_PROCESS = "1" }
dependencies = ["start", "wait-one", "test-playwright", "stop"]
[tasks.wait-one]
script = '''
sleep 1
'''

View File

@@ -0,0 +1,13 @@
extend = [
{ path = "./client-process.toml" },
{ path = "./server-process.toml" },
]
[tasks.start]
dependencies = ["maybe-start-server", "maybe-start-client"]
[tasks.status]
dependencies = ["server-status", "client-status"]
[tasks.stop]
dependencies = ["stop-client", "stop-server"]

View File

@@ -0,0 +1,34 @@
[tasks.start-server]
[tasks.stop-server]
condition = { env_set = ["SERVER_PROCESS_NAME"] }
script = '''
if [ ! -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
pkill -ef ${SERVER_PROCESS_NAME}
fi
'''
[tasks.server-status]
condition = { env_set = ["SERVER_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
echo " ${SERVER_PROCESS_NAME} is not running"
else
echo " ${SERVER_PROCESS_NAME} is up"
fi
'''
[tasks.maybe-start-server]
condition = { env_set = ["SERVER_PROCESS_NAME"] }
script = '''
YELLOW="\e[0;33m"
RESET="\e[0m"
if [ -z $(pidof ${SERVER_PROCESS_NAME}) ]; then
echo " Starting ${SERVER_PROCESS_NAME}"
echo " ${YELLOW}>> Run cargo make stop to end process${RESET}"
cargo make start-server ${@} &
else
echo " ${SERVER_PROCESS_NAME} is already started"
fi
'''

View File

@@ -1,18 +1,10 @@
[env]
CLIENT_PROCESS_NAME = "trunk"
[tasks.build]
command = "trunk"
args = ["build"]
[tasks.start-trunk]
[tasks.start-client]
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,30 @@
[tasks.start-webdriver]
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
if command -v chromedriver; then
if [ -z $(pidof chromedriver) ]; then
chromedriver --port=4444 &
fi
else
echo "${RED}${BOLD}ERROR${RESET} - chromedriver is required by this task"
exit 1
fi
'''
[tasks.stop-webdriver]
script = '''
pkill -f "chromedriver"
'''
[tasks.webdriver-status]
script = '''
if [ -z $(pidof chromedriver) ]; then
echo chromedriver is not running
else
echo chromedriver is up
fi
'''

View File

@@ -1,14 +1,5 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,6 +2,6 @@
This example creates a simple counter in a client side rendered app with Rust and WASM!
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.
## Getting Started
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
See the [Examples README](../README.md) for setup and run instructions.

View File

@@ -5,15 +5,14 @@ use leptos::*;
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleCounter(
cx: Scope,
/// The starting value for the counter
initial_value: i32,
/// The change that should be applied each time the button is clicked.
step: i32,
) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
let (value, set_value) = create_signal(initial_value);
view! { cx,
view! {
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>

View File

@@ -4,8 +4,8 @@ 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,
mount_to_body(|| {
view! {
<SimpleCounter
initial_value=0
step=1

View File

@@ -15,7 +15,7 @@ fn clear() {
// note that we start at the initial value of 10
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|| view! { <SimpleCounter initial_value=10 step=1/> },
);
// now we extract the buttons by iterating over the DOM
@@ -32,16 +32,17 @@ fn clear() {
// now let's test the <div> against the expected value
// we can do this by testing its `outerHTML`
let runtime = create_runtime();
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system, just to render the
// test case
run_scope(create_runtime(), |cx| {
{
// it's as if we're creating it with a value of 0, right?
let (value, _set_value) = create_signal(cx, 0);
let (value, _set_value) = create_signal(0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
@@ -52,7 +53,7 @@ fn clear() {
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
}
);
// There's actually an easier way to do this...
@@ -61,10 +62,12 @@ fn clear() {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|| view! { <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
runtime.dispose();
}
#[wasm_bindgen_test]
@@ -75,7 +78,7 @@ fn inc() {
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> },
|| view! { <SimpleCounter initial_value=0 step=1/> },
);
// You can do testing with vanilla DOM operations
@@ -118,12 +121,14 @@ fn inc() {
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
let runtime = create_runtime();
// Or you can test against a sample view!
assert_eq!(
div.outer_html(),
run_scope(create_runtime(), |cx| {
let (value, _) = create_signal(cx, 0);
view! { cx,
{
let (value, _) = create_signal(0);
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
@@ -132,17 +137,17 @@ fn inc() {
</div>
}
}
.outer_html())
.outer_html()
);
inc.click();
assert_eq!(
div.outer_html(),
run_scope(create_runtime(), |cx| {
{
// because we've clicked, it's as if the signal is starting at 1
let (value, _) = create_signal(cx, 1);
view! { cx,
let (value, _) = create_signal(1);
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
@@ -151,6 +156,8 @@ fn inc() {
</div>
}
}
.outer_html())
.outer_html()
);
runtime.dispose();
}

View File

@@ -25,7 +25,7 @@ leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
log = "0.4"
gloo-net = { git = "https://github.com/rustwasm/gloo" }
wasm-bindgen = "=0.2.87"
wasm-bindgen = "0.2.87"
serde = { version = "1", features = ["derive"] }
[features]

View File

@@ -1,11 +1,8 @@
extend = [{ path = "../cargo-make/main.toml" }]
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[env]
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
CLIENT_PROCESS_NAME = "counter_isomorphic"

View File

@@ -2,42 +2,6 @@
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
## Client Side Rendering
For this example the server must store the counter state since it can be modified by many users.
This means it is not possible to produce a working CSR-only version as a non-static server is required.
## Getting Started
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. For examples with CSS you also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```
See the [Examples README](../README.md) for setup and run instructions.

View File

@@ -15,13 +15,12 @@ cfg_if! {
}
}
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
#[server]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[server(AdjustServerCount, "/api")]
#[server]
pub async fn adjust_server_count(
delta: i32,
msg: String,
@@ -33,16 +32,16 @@ pub async fn adjust_server_count(
Ok(new)
}
#[server(ClearServerCount, "/api")]
#[server]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
COUNT.store(0, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&0).await;
Ok(0)
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! { cx,
pub fn Counters() -> impl IntoView {
provide_meta_context();
view! {
<Router>
<header>
<h1>"Server-Side Counters"</h1>
@@ -67,10 +66,24 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<main>
<Routes>
<Route path="" view=Counter/>
<Route path="form" view=FormCounter/>
<Route path="multi" view=MultiuserCounter/>
<Route path="multi" view=NotFound/>
<Route
path=""
view=|| {
view! { <Counter/> }
}
/>
<Route
path="form"
view=|| {
view! { <FormCounter/> }
}
/>
<Route
path="multi"
view=|| {
view! { <MultiuserCounter/> }
}
/>
</Routes>
</main>
</Router>
@@ -82,12 +95,11 @@ pub fn Counters(cx: Scope) -> impl IntoView {
// it's invalidated by one of the user's own actions
// This is the typical pattern for a CRUD app
#[component]
pub fn Counter(cx: Scope) -> impl IntoView {
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
let clear = create_action(cx, |_| clear_server_count());
pub fn Counter() -> impl IntoView {
let dec = create_action(|_| adjust_server_count(-1, "decing".into()));
let inc = create_action(|_| adjust_server_count(1, "incing".into()));
let clear = create_action(|_| clear_server_count());
let counter = create_resource(
cx,
move || {
(
dec.version().get(),
@@ -98,20 +110,16 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|_| get_server_count(),
);
let value = move || {
counter
.read(cx)
.map(|count| count.unwrap_or(0))
.unwrap_or(0)
};
let value =
move || counter.get().map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter.read(cx).and_then(|res| match res {
counter.get().and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
};
view! { cx,
view! {
<div>
<h2>"Simple Counter"</h2>
<p>
@@ -126,7 +134,7 @@ pub fn Counter(cx: Scope) -> impl IntoView {
{move || {
error_msg()
.map(|msg| {
view! { cx, <p>"Error: " {msg.to_string()}</p> }
view! { <p>"Error: " {msg.to_string()}</p> }
})
}}
</div>
@@ -137,12 +145,13 @@ pub fn Counter(cx: Scope) -> impl IntoView {
// It uses the same invalidation pattern as the plain counter,
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter(cx: Scope) -> impl IntoView {
let adjust = create_server_action::<AdjustServerCount>(cx);
let clear = create_server_action::<ClearServerCount>(cx);
pub fn FormCounter() -> impl IntoView {
// these struct names are auto-generated by #[server]
// they are just the PascalCased versions of the function names
let adjust = create_server_action::<AdjustServerCount>();
let clear = create_server_action::<ClearServerCount>();
let counter = create_resource(
cx,
move || (adjust.version().get(), clear.version().get()),
|_| {
log::debug!("FormCounter running fetcher");
@@ -151,19 +160,23 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
);
let value = move || {
log::debug!("FormCounter looking for value");
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
counter.get().and_then(|n| n.ok()).unwrap_or(0)
};
view! { cx,
view! {
<div>
<h2>"Form Counter"</h2>
<p>
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
</p>
<div>
// calling a server function is the same as POSTing to its API URL
// so we can just do that with a form and button
<ActionForm action=clear>
<input type="submit" value="Clear"/>
</ActionForm>
// We can submit named arguments to the server functions
// by including them as input values with the same name
<ActionForm action=adjust>
<input type="hidden" name="delta" value="-1"/>
<input type="hidden" name="msg" value="form value down"/>
@@ -185,12 +198,11 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
// Whenever another user updates the value, it will update here
// This is the primitive pattern for live chat, collaborative editing, etc.
#[component]
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
pub fn MultiuserCounter() -> impl IntoView {
let dec =
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc =
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(cx, |_| clear_server_count());
create_action(|_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(|_| clear_server_count());
#[cfg(not(feature = "ssr"))]
let multiplayer_value = {
@@ -200,7 +212,6 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source
.subscribe("message")
.unwrap()
@@ -214,14 +225,14 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
}),
);
on_cleanup(cx, move || source.close());
on_cleanup(move || source.close());
s
};
#[cfg(feature = "ssr")]
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
let (multiplayer_value, _) = create_signal(None::<i32>);
view! { cx,
view! {
<div>
<h2>"Multi-User Counter"</h2>
<p>
@@ -238,14 +249,3 @@ 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> }
}

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