Compare commits

...

179 Commits
1751 ... v0.5.4

Author SHA1 Message Date
Greg Johnston
b95a79240e v0.5.4 2023-11-28 18:46:51 -05:00
Alexis Fontaine
8e374efe8d fix: invalid attribute value for aria-current (#2089) 2023-11-28 15:23:16 -05:00
Greg Johnston
b578660624 docs: make it easy to see how to run each example in its README (#2085) 2023-11-28 11:47:56 -05:00
Greg Johnston
d6ee2a37f4 v0.5.3 2023-11-27 19:38:33 -05:00
Greg Johnston
18a92bbfd8 fix: improved rust-analyzer support in #[component] macro (#2075) 2023-11-27 19:37:43 -05:00
Greg Johnston
4e8c3accf2 fix: make prop serialization opt-in for devtools (closes #1952) (#2081) 2023-11-27 16:35:31 -05:00
Joseph Cruz
a8e25af523 ci(leptos): run ci on change instead of check (#2061)
* ci: run ci examples on leptos change

* chore(ci): simulate leptos source change

* ci(todo_app_sqlite_csr): increase retries

* ci: delete check examples workflow

* ci: rename ci examples workflow

* ci: run ci examples with stable toolchain

* chore(ci): remove simulated change

* ci: delete check stable workflow
2023-11-24 14:59:13 -05:00
Greg Johnston
d531848db5 fix: dispose previous route or outlet before rendering new one (closes #2070) (#2071) 2023-11-24 14:51:51 -05:00
hpepper
670f415565 docs: add instruction to install trunk to examples/README.md (#2064)
Co-authored-by: Henry Pepper <henry>
2023-11-24 14:06:02 -05:00
Greg Johnston
061213ca78 fix: correctly mark Trigger as clean when it is re-tracked (closes #1948, #2048) (#2059) 2023-11-22 09:29:25 -05:00
Greg Johnston
0ce4ee8a7a docs: add warning for nested Fn in attribute (see #2023) (#2045) 2023-11-22 07:35:20 -05:00
Greg Johnston
1cd6603da0 ci(examples): fix portal test (#2051) 2023-11-20 20:39:19 -05:00
Andrew Wheeler(Genusis)
453911e6fc examples: updated axum session to latest 0.9 in examples (#2049)
* updated axum_database_sessions to axum_session along with axum_sessions_auth to axum_session_auth

* updated to axum session 0.9
2023-11-20 20:33:31 -05:00
Greg Johnston
cb6267ad08 feat: <Provider/> component to fix context shadowing (closes #2038) (#2040) 2023-11-19 20:24:36 -05:00
Ken
4518d3c89f Have fetch example conform to docs guidance around using <ErrorBoundary> and <Transition> in conjunction (#2035)
* put `<ErrorBoundary>` inside `<Transition>`

* fix indentation
2023-11-18 08:24:30 -05:00
Greg Johnston
e47a619556 examples: add CSR with server functions example (closes #1975) (#2031) 2023-11-18 08:24:15 -05:00
Daniel Mantei
414f5fc393 docs: reorganize deployment section (#2036)
* Update mdbook-admonish book dependency

* Move "Optimizing Binary Size" to Deploy.. chapter

* Minor text updates to the Deployment section
2023-11-17 15:40:20 -05:00
martin frances
362e3bc603 chore: stop using std::fmt, instead used core::fmt. (#2033) 2023-11-17 15:36:13 -05:00
taohua
4d549f70c9 docs: fix misnamed form field in <Form/> example (#2024)
Co-authored-by: datewu <>
2023-11-17 15:27:06 -05:00
Chris
85dd726d43 docs: ActionForm examples for indexing into struct fields (#2017)
Co-authored-by: chrisp60 <gh@cperry.me>
2023-11-17 15:22:11 -05:00
blorbb
24febe11f3 feat: impl Default for TextProp (#2016) 2023-11-17 15:20:03 -05:00
Greg Johnston
64b1e9bed3 fix: use create_effect for <Portal/> to avoid hydration issues (closes #2010) (#2029) 2023-11-17 15:19:07 -05:00
Greg Johnston
68c91a732d fix: allow nested functions in Attribute (closes #2023) (#2027) 2023-11-15 11:00:15 -05:00
blorbb
8573f22d96 fix: re-export slice! macro (#2008) 2023-11-11 06:47:15 -05:00
Greg Johnston
61c7ff4256 docs: add note about context shadowing (closes #1986) (#2015) 2023-11-10 18:04:22 -05:00
Greg Johnston
860d887931 chore: remove duplicate benchmarks in leptos_reactive (#2014) 2023-11-10 15:53:32 -05:00
Chris
5e929a75fa feat: Action::new and Action::server (#1998) 2023-11-10 15:53:20 -05:00
Greg Johnston
d82cf0b76a docs: remove outdated APP_ENVIRONMENT variable (#2013) 2023-11-10 14:27:45 -05:00
Greg Johnston
cb7e07496a docs: fix CodeSandbox for resources (#2002) 2023-11-07 20:11:30 -05:00
Greg Johnston
17881c5c6e docs: fix chapter 10 CodeSandbox 2023-11-07 20:07:38 -05:00
Greg Johnston
2e816b26aa benchmarks: get benchmarks directory working with updated tachys 2023-11-07 20:03:35 -05:00
Gabriel Hansson
68d67c9e92 book: Fix <Body/> link in metadata.md (#1999) 2023-11-07 16:16:47 -05:00
Greg Johnston
0dea6fdcea fix: correctly reset island/not-island state of SSRed Suspense streaming into island (closes #1996) (#2000) 2023-11-07 16:16:27 -05:00
Greg Johnston
530dcff86a examples: remove incorrect CSR information for hackernews_js_fetch example (#1997) 2023-11-06 14:45:26 -05:00
Greg Johnston
9d9a4932b3 fix: run <ErrorBoundary/> in a child so siblings don't collide (closes #1987) (#1991) 2023-11-05 21:29:35 -05:00
Greg Johnston
bfb67d45e8 examples: fix style.css path (closes #1992) (#1994) 2023-11-05 21:29:17 -05:00
Greg Johnston
b1e8105442 fix: treat Suspense as containing a Set of resources, not a counter (closes Suspense only working with a single Resource (closes #1805, closes #1905) (#1985) 2023-11-04 11:03:36 -04:00
Greg Johnston
7aced17976 docs: clarify need to provide context to both rendering and server function handler (#1983) 2023-11-03 18:34:50 -04:00
Gabriel Hansson
191b40b2ac docs: point leptos_server docs.rs url to latest version. (#1982) 2023-11-03 17:05:42 -04:00
Gabriel Hansson
15ca5bec61 docs: fix <Transition/> url in 12_transition.md (#1980) 2023-11-03 17:00:26 -04:00
Gabriel Hansson
ba4d226004 docs: Fix 08_parent_child.md callback example code. (#1976) 2023-11-03 16:58:45 -04:00
Chris
3adfd334df fix: leptos_router::params_map! (#1973)
Fixing implementation comes with the benefit of knocking a crate out of
the deps tree (`common_macros`).
2023-11-02 16:29:50 -04:00
martin frances
d7ca5f2e96 chore: typed-builder and typed-builder-macro - bumped version numbers. (#1958) 2023-10-29 21:49:48 -04:00
Chris
67bdb3498f docs: switch feature flag stable to nightly (#1959) 2023-10-29 21:48:53 -04:00
Greg Johnston
9e9386b223 does this make clippy happy in CI? (#1965) 2023-10-29 21:48:33 -04:00
SleeplessOne1917
4029de2d42 feat: impl IntoAttribute for TextProp (#1925) 2023-10-27 17:10:09 -04:00
Greg Johnston
777095670e fix: add leptos_axum::build_static_routes (closes #1843) (#1855) 2023-10-27 17:09:52 -04:00
koopa
a11c6303e2 feat: allow arbitrary attributes for <A/> component (#1953) 2023-10-27 15:30:30 -04:00
Daniél Kerkmann
3394e316b7 docs: add ignoring #[server] macro for helix as well (#1951)
Update helix configuration for the newest version.
To be consistent, adding the `server` ignore entry to helix as well.
Also sorting the parameters alphabetically.
2023-10-27 13:55:00 -04:00
Ari Seyhun
4b0437394c feat: impl IntoAttribute for Cow<'static, str> (#1945) 2023-10-27 13:48:43 -04:00
Ari Seyhun
d10a566e48 feat: add new method to Trigger (#1935) 2023-10-27 13:48:15 -04:00
martin frances
e0cca3e7a3 workflows: bumped tj-actions/changed-files to @39. (#1942) 2023-10-27 13:47:56 -04:00
martin frances
0c8ab7c725 workflows: bump setup-node to version 4. (#1944) 2023-10-27 13:47:33 -04:00
Ari Seyhun
a2bef05a4b perf: IntoView and IntoAttribute for std::fmt::Arguments improvements (#1947)
* fix: use static str when possible in `std::fmt::Arguments` in views

* feat: impl `IntoAttribute` for `std::fmt::Arguments`
2023-10-27 13:42:27 -04:00
Greg Johnston
6361985fb1 fix: relax 'static bound on as_child_of_current_owner (#1955) 2023-10-27 13:20:34 -04:00
Greg Johnston
ad290f5ed2 chore: update README.md to remove note about 0.5 2023-10-24 22:12:12 -04:00
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
353 changed files with 10356 additions and 2573 deletions

View File

@@ -1,4 +1,4 @@
name: Check Examples
name: CI Examples
on:
push:
@@ -11,12 +11,10 @@ on:
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
test:
name: Check
name: CI
needs: [get-leptos-changed, get-examples-matrix]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
@@ -25,5 +23,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -1,4 +1,4 @@
name: Check stable
name: CI Stable Examples
on:
push:
@@ -13,7 +13,7 @@ jobs:
uses: ./.github/workflows/get-leptos-changed.yml
test:
name: Check
name: CI
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
strategy:
@@ -22,5 +22,5 @@ jobs:
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
cargo_make_task: "ci"
toolchain: stable

View File

@@ -20,13 +20,13 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get example project directories that changed
id: changed-dirs
uses: tj-actions/changed-files@v36
uses: tj-actions/changed-files@v39
with:
dir_names: true
dir_names_max_depth: "2"
@@ -34,6 +34,7 @@ jobs:
examples
!examples/cargo-make
!examples/gtk
!examples/hackernews_js_fetch
!examples/Makefile.toml
!examples/*.md
json: true

View File

@@ -15,13 +15,13 @@ jobs:
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get example files that changed
id: changed-files
uses: tj-actions/changed-files@v36
uses: tj-actions/changed-files@v39
with:
files: |
examples

View File

@@ -15,7 +15,7 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install JQ Tool
uses: mbround18/install-jq@v1
@@ -23,12 +23,12 @@ jobs:
- 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 |
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"

View File

@@ -15,11 +15,11 @@ jobs:
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
uses: tj-actions/changed-files@v39
with:
files: |
integrations

View File

@@ -12,7 +12,7 @@ jobs:
contents: write # To push a branch
pull-requests: write # To create a PR from that branch
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install mdbook

View File

@@ -53,7 +53,7 @@ jobs:
run: trunk --version
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18

View File

@@ -1,26 +0,0 @@
name: CI Examples
on:
workflow_dispatch:
push:
tags:
- v*
schedule:
# Run once a day at 3:00 AM EST
- cron: "0 8 * * *"
jobs:
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
test:
name: CI
needs: [get-examples-matrix]
strategy:
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: "ci"
toolchain: nightly

View File

@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.5.0-rc2"
version = "0.5.4"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.5.0-rc2" }
leptos_dom = { path = "./leptos_dom", version = "0.5.0-rc2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-rc2" }
leptos_macro = { path = "./leptos_macro", version = "0.5.0-rc2" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-rc2" }
leptos_server = { path = "./leptos_server", version = "0.5.0-rc2" }
server_fn = { path = "./server_fn", version = "0.5.0-rc2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-rc2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-rc2" }
leptos_config = { path = "./leptos_config", version = "0.5.0-rc2" }
leptos_router = { path = "./router", version = "0.5.0-rc2" }
leptos_meta = { path = "./meta", version = "0.5.0-rc2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-rc2" }
leptos = { path = "./leptos", version = "0.5.4" }
leptos_dom = { path = "./leptos_dom", version = "0.5.4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.4" }
leptos_macro = { path = "./leptos_macro", version = "0.5.4" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.4" }
leptos_server = { path = "./leptos_server", version = "0.5.4" }
server_fn = { path = "./server_fn", version = "0.5.4" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.4" }
leptos_config = { path = "./leptos_config", version = "0.5.4" }
leptos_router = { path = "./router", version = "0.5.4" }
leptos_meta = { path = "./meta", version = "0.5.4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.4" }
[profile.release]
codegen-units = 1

View File

@@ -10,6 +10,8 @@
[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
@@ -46,10 +48,6 @@ pub fn main() {
}
```
### Important Note
This example, and the entire `main` branch, now reflect the upcoming `0.5.0` release. You can use `0.5.0` with the `0.5.0-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.

View File

@@ -4,12 +4,17 @@ 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",
"experimental-islands",
"ssr",
] }
leptos = { path = "../leptos", features = ["ssr", "nightly"] }
leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] }
tachydom = { git = "https://github.com/gbj/tachys", features = [
"nightly",
"leptos",
] }
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { version = "0.20", features = ["ssr"] }
tokio-test = "0.4"
@@ -24,7 +29,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, || {
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));
}
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, || {
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));
}
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,16 +50,12 @@ fn leptos_narrowing_down(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
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);
})
.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();
@@ -77,16 +67,13 @@ fn leptos_fanning_out(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
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);
})
.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();
@@ -97,145 +84,36 @@ fn leptos_narrowing_update(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
create_scope(runtime, || {
let acc = Rc::new(Cell::new(0));
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());
}
});
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 || {
let (r, w) = create_signal(0);
create_isomorphic_effect({
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::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
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::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
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::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
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(|| {
@@ -257,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(|| {
@@ -282,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(|| {
@@ -305,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(|| {
@@ -325,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(|| {
@@ -339,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);
@@ -353,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()
});
@@ -362,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(|| {
@@ -376,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(initial: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial);
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,53 @@ 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-0-1\"><h1 data-hk=\"0-0-0-2\">Welcome to our benchmark page.</h1><p data-hk=\"0-0-0-3\">Here&#x27;s some introductory text.</p><div data-hk=\"0-0-0-5\"><button data-hk=\"0-0-0-6\">-1</button><span data-hk=\"0-0-0-7\">Value: <!>1<!--hk=0-0-0-8-->!</span><button data-hk=\"0-0-0-9\">+1</button></div><!--hk=0-0-0-4--><div data-hk=\"0-0-0-11\"><button data-hk=\"0-0-0-12\">-1</button><span data-hk=\"0-0-0-13\">Value: <!>2<!--hk=0-0-0-14-->!</span><button data-hk=\"0-0-0-15\">+1</button></div><!--hk=0-0-0-10--><div data-hk=\"0-0-0-17\"><button data-hk=\"0-0-0-18\">-1</button><span data-hk=\"0-0-0-19\">Value: <!>3<!--hk=0-0-0-20-->!</span><button data-hk=\"0-0-0-21\">+1</button></div><!--hk=0-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, RenderHtml};
use tachydom::html::element::ElementChild;
use tachydom::html::attribute::global::ClassAttribute;
use tachydom::html::attribute::global::GlobalAttributes;
use tachydom::html::attribute::global::OnAttribute;
use tachydom::renderer::dom::Dom;
let rt = create_runtime();
b.iter(|| {
fn counter(initial: i32) -> impl Render<Dom> + RenderHtml<Dom> {
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 rendered = view! {
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
{counter(1)}
{counter(2)}
{counter(3)}
</main>
}.to_html();
assert_eq!(
rendered,
"<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

@@ -192,7 +192,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
<For
each=filtered_todos
key=|todo| todo.id
view=move |todo: Todo| {
children=move |todo: Todo| {
view! { <Todo todo=todo.clone()/> }
}
/>

View File

@@ -2,6 +2,7 @@ use test::Bencher;
mod leptos;
mod sycamore;
mod tachys;
mod tera;
mod yew;
@@ -17,13 +18,29 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
});
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, RenderHtml};
let rendered = TodoMVC(Todos::new()).to_html();
assert_eq!(
rendered,
"<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 +59,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 +76,33 @@ 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, RenderHtml};
let rendered = TodoMVC(Todos::new_with_1000()).to_html();
assert!(rendered.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 +121,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 +130,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,333 @@
pub use leptos_reactive::*;
use miniserde::*;
use tachy_maccy::view;
use tachydom::{
html::{
attribute::global::{ClassAttribute, GlobalAttributes, OnAttribute},
element::ElementChild,
},
renderer::dom::Dom,
view::{keyed::keyed, Render, RenderHtml},
};
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<Dom> + RenderHtml<Dom> {
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">
{move || {
keyed(filtered_todos.get(), |todo| todo.id, Todo)
}}
</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<Dom> + RenderHtml<Dom> {
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

@@ -12,3 +12,4 @@ mdbook serve
```
It should be available at `http://localhost:3000`.

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 = "3.0.1" # do not edit: managed by `mdbook-admonish install`

View File

@@ -0,0 +1,345 @@
@charset "UTF-8";
:root {
--md-admonition-icon--admonish-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--admonish-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--admonish-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--admonish-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--admonish-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--admonish-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--admonish-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--admonish-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--admonish-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--admonish-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--admonish-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--admonish-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);
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
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;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
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(.admonish-note) {
border-color: #448aff;
}
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(68, 138, 255, 0.1);
}
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #448aff;
mask-image: var(--md-admonition-icon--admonish-note);
-webkit-mask-image: var(--md-admonition-icon--admonish-note);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-abstract, .admonish-summary, .admonish-tldr) {
border-color: #00b0ff;
}
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 176, 255, 0.1);
}
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00b0ff;
mask-image: var(--md-admonition-icon--admonish-abstract);
-webkit-mask-image: var(--md-admonition-icon--admonish-abstract);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-info, .admonish-todo) {
border-color: #00b8d4;
}
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 184, 212, 0.1);
}
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00b8d4;
mask-image: var(--md-admonition-icon--admonish-info);
-webkit-mask-image: var(--md-admonition-icon--admonish-info);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-tip, .admonish-hint, .admonish-important) {
border-color: #00bfa5;
}
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 191, 165, 0.1);
}
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00bfa5;
mask-image: var(--md-admonition-icon--admonish-tip);
-webkit-mask-image: var(--md-admonition-icon--admonish-tip);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-success, .admonish-check, .admonish-done) {
border-color: #00c853;
}
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 200, 83, 0.1);
}
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00c853;
mask-image: var(--md-admonition-icon--admonish-success);
-webkit-mask-image: var(--md-admonition-icon--admonish-success);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-question, .admonish-help, .admonish-faq) {
border-color: #64dd17;
}
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(100, 221, 23, 0.1);
}
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #64dd17;
mask-image: var(--md-admonition-icon--admonish-question);
-webkit-mask-image: var(--md-admonition-icon--admonish-question);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-warning, .admonish-caution, .admonish-attention) {
border-color: #ff9100;
}
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 145, 0, 0.1);
}
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff9100;
mask-image: var(--md-admonition-icon--admonish-warning);
-webkit-mask-image: var(--md-admonition-icon--admonish-warning);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-failure, .admonish-fail, .admonish-missing) {
border-color: #ff5252;
}
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 82, 82, 0.1);
}
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff5252;
mask-image: var(--md-admonition-icon--admonish-failure);
-webkit-mask-image: var(--md-admonition-icon--admonish-failure);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-danger, .admonish-error) {
border-color: #ff1744;
}
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 23, 68, 0.1);
}
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff1744;
mask-image: var(--md-admonition-icon--admonish-danger);
-webkit-mask-image: var(--md-admonition-icon--admonish-danger);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-bug) {
border-color: #f50057;
}
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(245, 0, 87, 0.1);
}
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #f50057;
mask-image: var(--md-admonition-icon--admonish-bug);
-webkit-mask-image: var(--md-admonition-icon--admonish-bug);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-example) {
border-color: #7c4dff;
}
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(124, 77, 255, 0.1);
}
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #7c4dff;
mask-image: var(--md-admonition-icon--admonish-example);
-webkit-mask-image: var(--md-admonition-icon--admonish-example);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.admonish-quote, .admonish-cite) {
border-color: #9e9e9e;
}
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(158, 158, 158, 0.1);
}
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #9e9e9e;
mask-image: var(--md-admonition-icon--admonish-quote);
-webkit-mask-image: var(--md-admonition-icon--admonish-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

@@ -17,6 +17,6 @@ understand Leptos.
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
**Important Note**: This current version of the book reflects the upcoming `0.5.0` release, which you can install as version `0.5.0-rc2`. The CodeSandbox versions of the examples still reflect `0.4` and earlier APIs and are in the process of being updated.
**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

@@ -23,26 +23,36 @@ cargo init leptos-tutorial
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos@0.5.0-rc2 --features=csr,nightly
cargo add leptos --features=csr,nightly
```
> **Note**: This version of the book reflects the upcoming Leptos 0.5.0 release. The CodeSandbox examples have not yet been updated from 0.4 and earlier versions.
> **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@0.5.0-rc2 --features=csr
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.

View File

@@ -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>
@@ -396,7 +396,6 @@ fn GlobalStateInput() -> impl IntoView {
fn main() {
leptos::mount_to_body(|| view! { <Option2/><Option3/> })
}
```
</details>

View File

@@ -7,6 +7,7 @@
- [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)
@@ -44,7 +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.md)
- [Deployment](./deployment/README.md)
- [Optimizing WASM Binary Size](./deployment/binary_size.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

@@ -13,8 +13,8 @@ VSCode `settings.json`:
```json
"rust-analyzer.procMacro.ignored": {
"leptos_macro": [
"server",
"component"
"component",
"server"
],
}
```
@@ -30,8 +30,8 @@ require('lspconfig').rust_analyzer.setup {
procMacro = {
ignored = {
leptos_macro = {
"server",
"component",
"server",
},
},
},
@@ -45,5 +45,18 @@ Helix, in `.helix/languages.toml`:
```toml
[[language]]
name = "rust"
config = { procMacro = {ignored = {leptos_macro = ["component"]}}}
[language-server.rust-analyzer]
config = { procMacro = { ignored = { leptos_macro = ["component", "server"] } } }
```
```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

@@ -30,7 +30,7 @@ To create a resource that simply runs once, you can pass a non-reactive, empty s
let once = create_resource(|| (), |_| async move { load_data().await });
```
To access the value you can use `.read()` or `.with(|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 for any `Resource<_, T>`, they always return `Option<T>`, not `T`: because its always possible that your resource is still loading.
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:
@@ -38,7 +38,7 @@ So, you can show the current state of a resource in your view:
let once = create_resource(|| (), |_| async move { load_data().await });
view! {
<h1>"My Data"</h1>
{move || match once.read() {
{move || match once.get() {
None => view! { <p>"Loading..."</p> }.into_view(),
Some(data) => view! { <ShowData data/> }.into_view()
}}
@@ -47,9 +47,9 @@ 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-x6h5j6?file=%2Fsrc%2Fmain.rs%3A2%2C3)
<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%3A2%2C3" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
@@ -74,7 +74,6 @@ fn App() -> impl IntoView {
// create_resource takes two arguments after its scope
let async_data = create_resource(
// the first is the "source signal"
count,
// the second is the loader
@@ -89,12 +88,12 @@ fn App() -> impl IntoView {
// that doesn't depend on anything: we just load it once
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()
.get()
.map(|value| format!("Server returned {value:?}"))
// This loading state will only show before the first load
.unwrap_or_else(|| "Loading...".into())
@@ -114,7 +113,7 @@ fn App() -> impl IntoView {
"Click me"
</button>
<p>
<code>"stable"</code>": " {move || stable.read()}
<code>"stable"</code>": " {move || stable.get()}
</p>
<p>
<code>"count"</code>": " {count}
@@ -129,9 +128,8 @@ fn App() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -8,7 +8,7 @@ let once = create_resource(count, |count| async move { load_a(count).await });
view! {
<h1>"My Data"</h1>
{move || match once.read() {
{move || match once.get() {
None => view! { <p>"Loading..."</p> }.into_view(),
Some(data) => view! { <ShowData data/> }.into_view()
}}
@@ -25,7 +25,7 @@ let b = create_resource(count2, |count| async move { load_b(count).await });
view! {
<h1>"My Data"</h1>
{move || match (a.read(), b.read()) {
{move || match (a.get(), b.get()) {
(Some(a), Some(b)) => view! {
<ShowA a/>
<ShowA b/>
@@ -53,12 +53,12 @@ view! {
<h2>"My Data"</h2>
<h3>"A"</h3>
{move || {
a.read()
a.get()
.map(|a| view! { <ShowA a/> })
}}
<h3>"B"</h3>
{move || {
b.read()
b.get()
.map(|b| view! { <ShowB b/> })
}}
</Suspense>
@@ -89,7 +89,7 @@ view! {
// `future` provides the `Future` to be resolved
future=|| fetch_monkeys(3)
// the data is bound to whatever variable name you provide
bind:data
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>
@@ -97,9 +97,9 @@ view! {
}
```
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
[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-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<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>
@@ -141,16 +141,15 @@ fn App() -> impl IntoView {
// and then whenever any resources has been resolved
<p>
"Your shouting name is "
{move || async_data.read()}
{move || async_data.get()}
</p>
</Suspense>
}
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

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

View File

@@ -91,9 +91,9 @@ view! {
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>
@@ -168,7 +168,7 @@ fn App() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```

View File

@@ -6,6 +6,7 @@ There are as many ways to deploy a web application as there are developers, let
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.)
3. See the chapter on "Optimizing WASM Binary Size" for additional tips and tricks to further improve the time-to-interactive metric for your WASM app on first load.
> 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).
@@ -54,7 +55,7 @@ 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/server/release/leptos_start /app/
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
@@ -63,7 +64,6 @@ 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

View File

@@ -1,4 +1,4 @@
# Appendix: Optimizing WASM Binary Size
# Optimizing WASM Binary Size
One of the primary downsides of deploying a Rust/WebAssembly frontend app is that splitting a WASM file into smaller chunks to be dynamically loaded is significantly more difficult than splitting a JavaScript bundle. There have been experiments like [`wasm-split`](https://emscripten.org/docs/optimizing/Module-Splitting.html) in the Emscripten ecosystem but at present theres no way to split and dynamically load a Rust/`wasm-bindgen` binary. This means that the whole WASM binary needs to be loaded before your app becomes interactive. Because the WASM format is designed for streaming compilation, WASM files are much faster to compile per kilobyte than JavaScript files. (For a deeper look, you can [read this great article from the Mozilla team](https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/) on streaming WASM compilation.)
@@ -59,13 +59,13 @@ And you'll need to add `panic = "abort"` to `[profile.release]` in `Cargo.toml`.
## Things to Avoid
There are certain crates that tend to inflate binary sizes. For example, the `regex` crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!) In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what `leptos_router` does on the few occasions it needs a regular expression.)
There are certain crates that tend to inflate binary sizes. For example, the `regex` crate with its default features adds about 500kb to a WASM binary (largely because it has to pull in Unicode table data!). In a size-conscious setting, you might consider avoiding regexes in general, or even dropping down and calling browser APIs to use the built-in regex engine instead. (This is what `leptos_router` does on the few occasions it needs a regular expression.)
In general, Rusts commitment to runtime performance is sometimes at odds with a commitment to a small binary. For example, Rust monomorphizes generic functions, meaning it creates a distinct copy of the function for each generic type its called with. This is significantly faster than dynamic dispatch, but increases binary size. Leptos tries to balance runtime performance with binary size considerations pretty carefully; but you might find that writing code that uses many generics tends to increase binary size. For example, if you have a generic component with a lot of code in its body and call it with four different types, remember that the compiler could include four copies of that same code. Refactoring to use a concrete inner function or helper can often maintain performance and ergonomics while reducing binary size.
## A Final Thought
Remember that in a server-rendered app, JS bundle size/WASM binary size affects only _one_ thing: time to interactivity on the first load. This is very important to a good user experiencenobody wants to click a button three times and have it do nothing because the interactive code is still loadingbut it is not the only important measure.
Remember that in a server-rendered app, JS bundle size/WASM binary size affects only _one_ thing: time to interactivity on the first load. This is very important to a good user experience: nobody wants to click a button three times and have it do nothing because the interactive code is still loadingbut it's not the only important measure.
Its especially worth remembering that streaming in a single WASM binary means all subsequent navigations are nearly instantaneous, depending only on any additional data loading. Precisely because your WASM binary is _not_ bundle split, navigating to a new route does not require loading additional JS/WASM, as it does in nearly every JavaScript framework. Is this copium? Maybe. Or maybe its just an honest trade-off between the two approaches!

View File

@@ -36,7 +36,7 @@ fn Home() -> 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
@@ -50,7 +50,7 @@ use stylers::style;
#[component]
pub fn App() -> impl IntoView {
let styler_class = style! { "App",
#two{
##two{
color: blue;
}
div.one{

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

@@ -28,7 +28,7 @@ Theres a very simple way to determine whether you should use a capital-S `<Sc
## `<Body/>` and `<Html/>`
There are even a couple elements designed to make semantic HTML and styling easier. [`<Html/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) lets you set the `lang` and `dir` on your `<html>` tag from your application code. `<Html/>` and [`<Body/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) both have `class` props that let you set their respective `class` attributes, which is sometimes needed by CSS frameworks for styling.
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.Body.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 `attr:` syntax:

View File

@@ -56,3 +56,45 @@ let on_submit = move |ev| {
}
}
```
## Complex Inputs
Server function arguments that are structs with nested serializable fields should make use of indexing notation of `serde_qs`.
```rust
use leptos::*;
use leptos_router::*;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
struct HeftyData {
first_name: String,
last_name: String,
}
#[component]
fn ComplexInput() -> impl IntoView {
let submit = Action::<VeryImportantFn, _>::server();
view! {
<ActionForm action=submit>
<input type="text" name="hefty_arg[first_name]" value="leptos"/>
<input
type="text"
name="hefty_arg[last_name]"
value="closures-everywhere"
/>
<input type="submit"/>
</ActionForm>
}
}
#[server]
async fn very_important_fn(
hefty_arg: HeftyData,
) -> Result<(), ServerFnError> {
assert_eq!(hefty_arg.first_name.as_str(), "leptos");
assert_eq!(hefty_arg.last_name.as_str(), "closures-everywhere");
Ok(())
}
```

View File

@@ -135,9 +135,9 @@ stop(); // stop watching
set_num.set(2); // (nothing happens)
```
[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)
[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/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>
<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>
@@ -293,10 +293,8 @@ fn EffectVsDerivedSignal() -> impl IntoView {
}
}
/*#[component]
#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in
/// The components Show wraps
children: Box<dyn Fn() -> Fragment>,
/// A closure that returns a bool that determines whether this thing runs
@@ -315,17 +313,16 @@ where
true => children().into_view(),
false => fallback().into_view(),
}
}*/
}
fn log(std::fmt::Display) {
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(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -108,8 +108,8 @@ 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.
**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("Bridget".to_string());

View File

@@ -143,8 +143,8 @@ pub fn ContactList() -> impl IntoView {
// the contact list
<For each=contacts
key=|contact| contact.id
view=|contact| todo!()
>
children=|contact| todo!()
/>
// the nested child, if any
// dont forget this!
<Outlet/>
@@ -204,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>
@@ -316,9 +316,8 @@ fn ContactInfo() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -82,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>
@@ -117,7 +117,7 @@ fn App() -> impl IntoView {
<Route
path="/contacts"
view=ContactList
>
>
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|| view! {
@@ -194,9 +194,8 @@ fn ContactInfo() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -35,9 +35,9 @@ The second argument here is a set of [`NavigateOptions`](https://docs.rs/leptos_
> 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>
@@ -70,7 +70,7 @@ fn App() -> impl IntoView {
<Route
path="/contacts"
view=ContactList
>
>
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|| view! {
@@ -147,9 +147,8 @@ fn ContactInfo() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -34,7 +34,7 @@ pub fn FormExample() -> impl IntoView {
view! {
<Form method="GET" action="">
<input type="search" name="search" value=search/>
<input type="search" name="q" value=search/>
<input type="submit"/>
</Form>
<Transition fallback=move || ()>
@@ -53,7 +53,7 @@ We can actually take it a step further and do something kind of clever:
```rust
view! {
<Form method="GET" action="">
<input type="search" name="search" value=search
<input type="search" name="q" value=search
oninput="this.form.requestSubmit()"
/>
</Form>
@@ -62,9 +62,9 @@ view! {
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>
@@ -172,9 +172,8 @@ pub fn FormExample() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -8,12 +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 modes of rendering 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)
1. [Out-of-Order Streaming](#out-of-order-streaming) (and a partially-blocked variant)
## Synchronous Rendering
@@ -67,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.
@@ -134,4 +134,23 @@ pub fn BlogPost() -> 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

@@ -158,4 +158,4 @@ In particular, youll sometimes see errors about the crate `mio` or missing th
You can use `create_effect` to specify that something should only run on the client, and not in the server. Is there a way to specify that something should run only on the server, and not the client?
In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html).)
In fact, there is. The next chapter will cover the topic of server functions in some detail. (In the meantime, you can check out their docs [here](https://docs.rs/leptos_server/latest/leptos_server/index.html).)

View File

@@ -86,7 +86,6 @@ fn clear() {
clear.click();
```rust
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
@@ -108,6 +107,7 @@ assert_eq!(
.outer_html()
})
);
}
````
### [`wasm-bindgen-test` with `counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable/tests/web)

View File

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

View File

@@ -145,9 +145,10 @@ Derived signals let you create reactive computed values that can be used in mult
places in your application with minimal overhead.
Note: Using a derived signal like this means that the calculation runs once per
signal change 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
are designed to solve this problem for expensive calculations.
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.
> #### Advanced Topic: Injecting Raw HTML
>
@@ -166,9 +167,9 @@ are designed to solve this problem for expensive calculations.
>
> [Click here for the full `view` macros docs](https://docs.rs/leptos/latest/leptos/macro.view.html).
[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)
[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-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>
<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>
@@ -227,7 +228,33 @@ fn App() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <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

@@ -404,9 +404,9 @@ and see the power of the `#[component]` macro combined with rust-analyzer here.
> 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.
[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)
[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-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>
<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>
@@ -473,7 +473,7 @@ fn App() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <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/>`
@@ -51,7 +51,8 @@ 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
// create a list of 5 signals
let length = 5;
let counters = (1..=length).map(|idx| create_signal(idx));
// each item manages a reactive view
@@ -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>
@@ -136,7 +137,6 @@ fn App() -> impl IntoView {
/// to add or remove any.
#[component]
fn StaticList(
/// How many counters to include in this list.
length: usize,
) -> impl IntoView {
@@ -172,7 +172,6 @@ fn StaticList(
/// remove counters.
#[component]
fn DynamicList(
/// The number of counters to begin with.
initial_length: usize,
) -> impl IntoView {
@@ -229,9 +228,9 @@ 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 |(id, (count, set_count))| {
children=move |(id, (count, set_count))| {
view! {
<li>
<button
@@ -258,9 +257,8 @@ fn DynamicList(
}
fn main() {
leptos::mount_to_body(|| view! { <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,7 +19,7 @@ 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. (The same is true for `checked` and `prop:checked`
`prop:value` for this reason. (The same is true for `checked` and `prop:checked`
on an `<input type="checkbox">`.)
```rust
@@ -44,28 +44,28 @@ view! {
```
> #### 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:
>
> 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
>
> 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?")
>
> el.setAttribute("value", "one more time?");
> // nothing should have changed. setting the "initial value" does nothing now
>
>
> // however...
> el.value = "But this works"
> 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.
@@ -137,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>
@@ -242,9 +242,8 @@ fn UncontrolledComponent() -> 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(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -283,9 +283,9 @@ view! {
}
```
[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>
@@ -376,9 +376,8 @@ fn App() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -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>
@@ -167,9 +167,8 @@ fn App() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -72,9 +72,7 @@ pub fn App() -> impl IntoView {
#[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>
@@ -90,10 +88,49 @@ 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
{
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
@@ -188,7 +225,7 @@ pub fn App() -> impl IntoView {
}
#[component]
pub fn Layout(d: WriteSignal<bool>) -> impl IntoView {
pub fn Layout(set_toggled: WriteSignal<bool>) -> impl IntoView {
view! {
<header>
<h1>"My Page"</h1>
@@ -200,7 +237,7 @@ pub fn Layout(d: WriteSignal<bool>) -> impl IntoView {
}
#[component]
pub fn Content(d: WriteSignal<bool>) -> impl IntoView {
pub fn Content(set_toggled: WriteSignal<bool>) -> impl IntoView {
view! {
<div class="content">
<ButtonD set_toggled/>
@@ -209,7 +246,7 @@ pub fn Content(d: WriteSignal<bool>) -> impl IntoView {
}
#[component]
pub fn ButtonD<F>(d: WriteSignal<bool>) -> impl IntoView {
pub fn ButtonD<F>(set_toggled: WriteSignal<bool>) -> impl IntoView {
todo!()
}
```
@@ -282,9 +319,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>
@@ -318,7 +355,6 @@ pub fn App() -> impl IntoView {
provide_context(SmallcapsContext(set_smallcaps));
view! {
<main>
<p
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
@@ -350,12 +386,10 @@ pub fn App() -> impl IntoView {
/// Button A receives a signal setter and updates the signal itself
#[component]
pub fn ButtonA(
/// Signal that will be toggled when the button is clicked.
setter: WriteSignal<bool>,
) -> impl IntoView {
view! {
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -367,7 +401,6 @@ pub fn ButtonA(
/// Button B receives a closure
#[component]
pub fn ButtonB<F>(
/// Callback that will be invoked when the button is clicked.
on_click: F,
) -> impl IntoView
@@ -375,7 +408,6 @@ where
F: Fn(MouseEvent) + 'static,
{
view! {
<button
on:click=on_click
>
@@ -401,7 +433,6 @@ where
#[component]
pub fn ButtonC() -> impl IntoView {
view! {
<button>
"Toggle Italics"
</button>
@@ -415,7 +446,6 @@ pub fn ButtonD() -> impl IntoView {
let setter = use_context::<SmallcapsContext>().unwrap().0;
view! {
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -425,9 +455,8 @@ pub fn ButtonD() -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -96,7 +96,7 @@ a component that takes its children and turns them into an unordered list.
```rust
#[component]
pub fn WrapsChildren(Children) -> impl IntoView {
pub fn WrapsChildren(children: Children) -> impl IntoView {
// Fragment has `nodes` field that contains a Vec<View>
let children = children()
.nodes
@@ -122,9 +122,9 @@ view! {
}
```
[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>
@@ -178,7 +178,6 @@ pub fn App() -> impl IntoView {
/// Displays a `render_prop` and some children within markup.
#[component]
pub fn TakesChildren<F, IV>(
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,
@@ -203,7 +202,7 @@ where
/// Wraps each child in an `<li>` and embeds them in a `<ul>`.
#[component]
pub fn WrapsChildren(Children) -> impl IntoView {
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
@@ -222,9 +221,8 @@ pub fn WrapsChildren(Children) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|| view! { <App/> })
leptos::mount_to_body(App)
}
```
</details>

View File

@@ -18,7 +18,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"hackernews",
"hackernews_axum",
"js-framework-benchmark",
"leptos-tailwind-axum",
"login_with_token_csr_only",
"parent_child",
"router",
@@ -27,8 +26,9 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"ssr_modes",
"ssr_modes_axum",
"suspense_tests",
"tailwind",
"tailwind_csr_trunk",
"tailwind_actix",
"tailwind_csr",
"tailwind_axum",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",

View File

@@ -6,7 +6,7 @@ The examples in this directory are all built and tested against the current `mai
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.4.9` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.4.9/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
@@ -40,6 +40,8 @@ Example projects depend on the following tools. Please install them as needed.
- [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_**)
- [Trunk](https://github.com/thedodd/trunk)
- Run `cargo install trunk`
- [Node Version Manager](https://github.com/nvm-sh/nvm/) (**_Optional_**)
- [Node.js](https://nodejs.org/)
- [pnpm](https://pnpm.io/) (**_Optional_**)

View File

@@ -8,3 +8,7 @@ CSS.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -5,3 +5,7 @@ This example creates a simple counter in a client side rendered app with Rust an
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -5,3 +5,7 @@ This example demonstrates how to use a function isomorphically, to run a server
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -5,3 +5,7 @@ This example creates a simple counter whose state is persisted and synced in the
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -5,3 +5,7 @@ This example is the same like the `counter` but it's written without using macro
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -5,3 +5,7 @@ This example showcases a basic leptos app with many counters. It is a good examp
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -65,7 +65,7 @@ pub fn Counters() -> impl IntoView {
<For
each=counters
key=|counter| counter.0
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
children=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! {
<Counter id value set_value/>
}

View File

@@ -5,3 +5,7 @@ This example showcases a basic Leptos app with many counters. It is a good examp
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -67,7 +67,7 @@ pub fn Counters() -> impl IntoView {
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
children=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}

View File

@@ -0,0 +1,2 @@
[build]
rustflags = ["--cfg=web_sys_unstable_apis"]

View File

@@ -0,0 +1,17 @@
[package]
name = "directives"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
web-sys = { version = "0.3", features = ["Clipboard", "Navigator"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2"
web-sys = "0.3"
gloo-timers = { version = "0.3", features = ["futures"] }

View File

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

View File

@@ -0,0 +1,11 @@
# Leptos Directives Example
This example showcases a basic leptos app that shows how to write and use directives.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,51 @@
use leptos::{ev::click, html::AnyElement, *};
pub fn highlight(el: HtmlElement<AnyElement>) {
let mut highlighted = false;
let _ = el.clone().on(click, move |_| {
highlighted = !highlighted;
if highlighted {
let _ = el.clone().style("background-color", "yellow");
} else {
let _ = el.clone().style("background-color", "transparent");
}
});
}
pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
let content = content.to_string();
let _ = el.clone().on(click, move |evt| {
evt.prevent_default();
evt.stop_propagation();
let _ = window()
.navigator()
.clipboard()
.expect("navigator.clipboard to be available")
.write_text(&content);
let _ = el.clone().inner_html(format!("Copied \"{}\"", &content));
});
}
#[component]
pub fn SomeComponent() -> impl IntoView {
view! {
<p>Some paragraphs</p>
<p>that can be clicked</p>
<p>in order to highlight them</p>
}
}
#[component]
pub fn App() -> impl IntoView {
let data = "Hello World!";
view! {
<a href="#" use:copy_to_clipboard=data>"Copy \"" {data} "\" to clipboard"</a>
<SomeComponent use:highlight />
}
}

View File

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

View File

@@ -0,0 +1,58 @@
use gloo_timers::future::sleep;
use std::time::Duration;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use directives::App;
use leptos::*;
use web_sys::HtmlElement;
#[wasm_bindgen_test]
async fn test_directives() {
mount_to_body(|| view! { <App/> });
sleep(Duration::ZERO).await;
let document = leptos::document();
let paragraphs = document.query_selector_all("p").unwrap();
assert_eq!(paragraphs.length(), 3);
for i in 0..paragraphs.length() {
println!("i: {}", i);
let p = paragraphs
.item(i)
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
assert_eq!(
p.style().get_property_value("background-color").unwrap(),
""
);
p.click();
assert_eq!(
p.style().get_property_value("background-color").unwrap(),
"yellow"
);
p.click();
assert_eq!(
p.style().get_property_value("background-color").unwrap(),
"transparent"
);
}
let a = document
.query_selector("a")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
assert_eq!(a.inner_html(), "Copy \"Hello World!\" to clipboard");
a.click();
assert_eq!(a.inner_html(), "Copied \"Hello World!\"");
}

View File

@@ -9,3 +9,7 @@ See the [Examples README](../README.md) for setup and run instructions.
## Testing
This project is configured to run start and stop of processes for integration tests wihtout the use of Cargo Leptos or Node.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -45,7 +45,7 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _)| *index
// renders each item to a view
view=move |error| {
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {

View File

@@ -28,7 +28,7 @@ async fn custom_handler(
move || {
provide_context(id.clone());
},
|| view! { <App/> },
App,
);
handler(req).await.into_response()
}
@@ -48,13 +48,13 @@ async fn main() {
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|| view! { <App/> }).await;
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.with_state(leptos_options);

View File

@@ -4,4 +4,8 @@ This example shows how to fetch data from the client in WebAssembly.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -89,15 +89,15 @@ pub fn fetch_example() -> impl IntoView {
}
/>
</label>
<ErrorBoundary fallback>
<Transition fallback=move || {
view! { <div>"Loading (Suspense Fallback)..."</div> }
}>
<Transition fallback=move || {
view! { <div>"Loading (Suspense Fallback)..."</div> }
}>
<ErrorBoundary fallback>
<div>
{cats_view}
</div>
</Transition>
</ErrorBoundary>
</ErrorBoundary>
</Transition>
</div>
}
}

View File

@@ -5,4 +5,4 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
gtk = { version = "0.5.0", package = "gtk4" }
gtk = { version = "0.5.0", package = "gtk4" }

View File

@@ -16,10 +16,7 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", features = [
"nightly",
"experimental-islands",
] }
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router", features = ["nightly"] }

View File

@@ -5,3 +5,7 @@ This example creates a basic clone of the Hacker News site. It showcases Leptos'
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` or `cargo leptos watch` to run this example.

View File

@@ -41,13 +41,11 @@ pub fn Stories() -> impl IntoView {
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
@@ -57,7 +55,6 @@ pub fn Stories() -> impl IntoView {
}.into_any()
} else {
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
@@ -79,24 +76,22 @@ pub fn Stories() -> impl IntoView {
<main class="news-list">
<div>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending=set_pending.into()
fallback=move || view! { <p>"Loading..."</p> }
set_pending
>
{move || match stories.get() {
None => None,
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
view=move |story: api::Story| {
view! {
<Story story/>
}
}
/>
let:story
>
<Story story/>
</For>
</ul>
}.into_any())
}
@@ -125,7 +120,7 @@ fn Story(story: api::Story) -> impl IntoView {
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
@@ -134,7 +129,7 @@ fn Story(story: api::Story) -> impl IntoView {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
@@ -147,7 +142,7 @@ fn Story(story: api::Story) -> impl IntoView {
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! {

View File

@@ -57,8 +57,10 @@ pub fn Story() -> impl IntoView {
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |comment| view! { <Comment comment /> }
/>
let:comment
>
<Comment comment />
</For>
</ul>
</div>
</div>
@@ -100,8 +102,10 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
<For
each=move || comments.clone()
key=|comment| comment.id
view=move |comment: api::Comment| view! { <Comment comment /> }
/>
let:comment
>
<Comment comment />
</For>
</ul>
}
})}

View File

@@ -5,3 +5,7 @@ This example creates a basic clone of the Hacker News site. It showcases Leptos'
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` or `cargo leptos watch` to run this example.

View File

@@ -15,7 +15,7 @@ pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
view= move | (_, error)| {
children=move | (_, error)| {
let error_string = error.to_string();
view! {

View File

@@ -18,7 +18,7 @@ if #[cfg(feature = "ssr")] {
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|| view! { <App/> }).await;
let routes = generate_route_list(App);
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");

View File

@@ -79,7 +79,7 @@ pub fn Stories() -> impl IntoView {
<div>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending=set_pending.into()
set_pending
>
{move || match stories.get() {
None => None,
@@ -90,12 +90,10 @@ pub fn Stories() -> impl IntoView {
<For
each=move || stories.clone()
key=|story| story.id
view=move | story: api::Story| {
view! {
<Story story/>
}
}
/>
let:story
>
<Story story/>
</For>
</ul>
}.into_any())
}

View File

@@ -57,8 +57,10 @@ pub fn Story() -> impl IntoView {
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move | comment| view! { <Comment comment /> }
/>
let:comment
>
<Comment comment />
</For>
</ul>
</div>
</div>
@@ -100,8 +102,10 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
<For
each=move || comments.clone()
key=|comment| comment.id
view=move | comment: api::Comment| view! { <Comment comment /> }
/>
let:comment
>
<Comment comment />
</For>
</ul>
}
})}

View File

@@ -1,5 +1,5 @@
[package]
name = "hackernews"
name = "hackernews_islands"
version = "0.1.0"
edition = "2021"
@@ -39,7 +39,6 @@ tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
wee_alloc = "0.4.5"
lazy_static = "1.4.0"
[features]

View File

@@ -5,3 +5,7 @@ This example creates a basic clone of the Hacker News site. It showcases Leptos'
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -15,7 +15,7 @@ pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
view= move | (_, error)| {
children= move | (_, error)| {
let error_string = error.to_string();
view! {

View File

@@ -36,12 +36,6 @@ cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
extern crate wee_alloc;
// Use `wee_alloc` as the global allocator.
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
pub fn hydrate() {
#[cfg(debug_assertions)]

View File

@@ -1,27 +1,26 @@
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use axum::{routing::get, Router};
pub use hackernews::fallback::file_and_error_handler;
pub use hackernews_islands::fallback::file_and_error_handler;
pub use leptos::*;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
pub use tower_http::{compression::CompressionLayer, services::ServeFile};
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use hackernews::*;
use hackernews_islands::*;
use ssr_imports::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|| view! { <App/> }).await;
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.with_state(leptos_options);
@@ -37,7 +36,7 @@ async fn main() {
// client-only stuff for Trunk
#[cfg(not(feature = "ssr"))]
pub fn main() {
use hackernews::*;
use hackernews_islands::*;
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -91,19 +91,17 @@ pub fn Stories() -> impl IntoView {
<div>
<Transition
fallback=|| ()
set_pending=set_pending.into()
set_pending
>
{move || stories.get().map(|story| story.map(|stories| view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
view=move |story: api::Story| {
view! {
<Story story/>
}
}
/>
let:story
>
<Story story/>
</For>
</ul>
}))}
</Transition>

View File

@@ -0,0 +1,103 @@
[package]
name = "hackernews-js-fetch"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.4.0", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6", default-features = false, optional = true }
tower = { version = "0.4.13", optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = [
"AbortController",
"AbortSignal",
"Request",
"Response",
] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = { version = "0.4.37", features = [
"futures-core-03-stream",
], optional = true }
axum-js-fetch = { version = "0.2.1", optional = true }
lazy_static = "1.4.0"
[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:tower",
"dep:http",
"dep:axum",
"dep:wasm-bindgen-futures",
"dep:axum-js-fetch",
"leptos/ssr",
"leptos_axum/wasm",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "http", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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