Compare commits

..

152 Commits

Author SHA1 Message Date
Greg Johnston
fa2be59895 feat: better error handling for ScopedFuture (#1810) 2023-09-29 17:12:56 -04:00
Sirius902
321c522fa5 fix: extra set of brackets in generate_head_metadata (#1811) 2023-09-29 17:12:46 -04:00
Greg Johnston
7378b8581a fix: broken Suspense when a resource loads immediately (closes #1805) (#1809) 2023-09-29 14:44:49 -04:00
Ben Wishovich
2d634364a9 feat: set Content-Type header for all Responses to text/html;charset="utf-8" (#1803) 2023-09-29 13:51:15 -04:00
Greg Johnston
f7adf6f73d examples: fix hackernews examples oops 2023-09-29 13:36:13 -04:00
martin frances
fb914e1a50 chore: bump outdated dependencies in leptos_macro (#1796)
-attribute-derive = { version = "0.6", features = ["syn-full"] }
+attribute-derive = { version = "0.8", features = ["syn-full"] }
-itertools = "0.10"
+itertools = "0.11"
2023-09-29 13:05:57 -04:00
Julien Scholz
772bb1d60c fix: improve rust-analyzer auto-completion (#1782) 2023-09-29 13:05:13 -04:00
Antonin Peronnet
bd4d2202ea feat: standardize on a Callback type that is Copy (#1795) 2023-09-29 13:04:53 -04:00
Greg Johnston
870808e63f feat: implement From<Fn() -> T> for Signal<T> (#1801) 2023-09-29 09:13:27 -04:00
Ben Wishovich
d7fff5a8ab fix: render_route error message and matching of non standard routes (#1799) 2023-09-29 09:10:59 -04:00
jquesada2016
609afce544 feat: Scoped Futures (#1761) 2023-09-28 15:20:18 -04:00
messense
181bcadbe2 feat(leptos_config): only enable toml feature for the config dependency (#1788) 2023-09-27 19:42:25 -04:00
Greg Johnston
3f2a9facf8 change: enable inline children for For by switching to children and bind: (#1773) 2023-09-26 14:24:02 -04:00
Saeed Andalib
c5c79234f1 docs: update working_with_signals.md (#1785)
Pulled the option number 2 out of the blockquote
2023-09-26 14:23:47 -04:00
Fangdun Tsai
de9fb5e382 chore(leptos_meta): enhance links in docs (#1783) 2023-09-25 20:34:11 -04:00
Greg Johnston
c9d132f007 change: use let: instead of bind: (#1774) 2023-09-25 20:33:36 -04:00
Greg Johnston
a1a9d41a7a updating SSR benchmarks to include tachys 2023-09-25 15:45:52 -04:00
Greg Johnston
73112c9faa Merge pull request #1779 from leptos-rs/docs-show
docs: fix `Show` docs reference to scope
2023-09-25 07:54:38 -04:00
Sean Aye
50678dafe1 feat: add JS Fetch integration support (#1554) 2023-09-25 07:51:25 -04:00
Greg Johnston
b1363a16ab docs: fix Show docs reference to scope 2023-09-23 12:46:41 -04:00
Greg Johnston
ae986e71fa change: only run create_local_resource in the browser (#1777) 2023-09-23 11:10:50 -04:00
Antonin Peronnet
0531831fe8 chore: add cargo-make and trunk in nix flake (#1763)
* add `cargo-make` dependency for nix
* add `trunk` dependency for nix
2023-09-23 11:10:24 -04:00
Greg Johnston
18eeee8e1f 0.5.0-rc3 2023-09-22 17:38:54 -04:00
Greg Johnston
d99269afac docs: error in view! macro if you use cx, (#1772) 2023-09-22 17:29:55 -04:00
Nico Burniske
38d1727e9c change: generate_route_list no longer async in any integration (#1485) 2023-09-22 15:42:58 -04:00
Greg Johnston
e0265252d7 fix: broken benchmarks (closes #1763) (#1771) 2023-09-22 15:41:44 -04:00
Fangdun Tsai
6cc92cee8d chore(leptos_hot_reload): apply lints suggestions (#1735) 2023-09-22 13:48:23 -04:00
Fangdun Tsai
1d392483b4 chore(leptos_marco): enhancement of document generation (#1768) 2023-09-22 13:32:58 -04:00
Village
3b864ac1a0 feat: Static Site Generation (#1649) 2023-09-22 13:32:09 -04:00
Danik Vitek
baa5ea83fa fix: reimplement Oco cloning (#1749) 2023-09-22 13:31:04 -04:00
Gabriel de Perthuis
d651400fa2 docs: better document the interaction of SsrModes with blocking resources (#1765)
Meant to address users making the same mistake as
https://github.com/leptos-rs/leptos/issues/1119
2023-09-22 12:58:28 -04:00
Fangdun Tsai
b729a658df chore(leptos_router): improve docs (#1769) 2023-09-22 12:56:49 -04:00
Gabriel de Perthuis
2c8f46466b feat: support default values for annotated server_fn arguments with #[server(default)] (#1762)
This allows form submission with checkbox inputs to work.
For example:

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

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

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

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

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

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

* test(ci): simulate leptos change

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

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

* refactor(examples): extract client process tasks

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

* test(error_boundary): click up arrow

* test(error_boundary): click down arrow

* test(error_boundary): type number

* test(error_boundary): clear number

* fix(build): clean trunk directories

* fix(test-report): detect unit tests

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

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

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

* chore: simulate leptos change

* chore: remove simulated change
2023-08-25 10:30:00 -04:00
Greg Johnston
c322ef38fd feat: signal traits should take associated types instead of generics (#1578) 2023-08-25 10:29:24 -04:00
Greg Johnston
c9cc493063 fix: fourth argument to server functions (#1585) 2023-08-25 10:28:54 -04:00
Joseph Cruz
fb48f7f117 fix(counters_stable): restore wasm tests (#1581) (#1582) 2023-08-24 16:33:01 -04:00
尹吉峰
c344e54cf6 feat: return an Effect from create_effect that can be disposed (#1571) 2023-08-24 10:24:10 -04:00
Greg Johnston
7306ecccbc feat: make struct name and path optional for server functions (#1573) 2023-08-24 10:22:35 -04:00
Greg Johnston
b98174db7a feat: support passing signals directly as attributes, classes, styles, and props on stable (#1577) 2023-08-24 10:22:14 -04:00
Greg Johnston
e48f66694d fix: runtime disposal time in render_to_string_async (#1574) 2023-08-24 10:22:00 -04:00
371 changed files with 13935 additions and 4952 deletions

View File

@@ -26,3 +26,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: nightly

View File

@@ -2,50 +2,25 @@ name: Check stable
on:
push:
branches: [main]
branches:
- main
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
branches:
- main
jobs:
get-leptos-changed:
uses: ./.github/workflows/get-leptos-changed.yml
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
name: Check
needs: [get-leptos-changed]
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- stable
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all examples
run: cargo make --profile=github-actions check-stable
directory: [examples/counters_stable, examples/counter_without_macros]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: stable

View File

@@ -29,3 +29,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -41,3 +41,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -34,8 +34,9 @@ jobs:
examples
!examples/cargo-make
!examples/gtk
!examples/hackernews_js_fetch
!examples/Makefile.toml
!examples/README.md
!examples/*.md
json: true
quotepath: false

View File

@@ -28,7 +28,7 @@ jobs:
!examples/cargo-make
!examples/gtk
!examples/Makefile.toml
!examples/README.md
!examples/*.md
- name: List example files that changed
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'

View File

@@ -25,7 +25,7 @@ jobs:
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v examples/README.md |
grep -v .md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |

View File

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

View File

@@ -9,6 +9,9 @@ on:
cargo_make_task:
required: true
type: string
toolchain:
required: true
type: string
env:
CARGO_TERM_COLOR: always
@@ -16,23 +19,17 @@ env:
jobs:
test:
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
runs-on: ubuntu-latest
steps:
# Setup environment
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
toolchain: ${{ inputs.toolchain }}
override: true
components: rustfmt
@@ -98,14 +95,17 @@ jobs:
- name: Maybe install playwright browser dependencies
run: |
playwright_count=$(find ${{inputs.directory}} -name playwright.config.ts | wc -l)
if [ $playwright_count -eq 1 ]; then
echo playwright required
sudo apt-get update
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
else
echo playwright is not required
fi
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
if [ ! -v $pw_dir ]; then
echo "Playwright required in $pw_dir"
cd $pw_dir
pnpm dlx playwright install --with-deps
else
echo Playwright is not required
fi
done
# Run Cargo Make Task
- name: ${{ inputs.cargo_make_task }}

View File

@@ -23,3 +23,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

2
.gitignore vendored
View File

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

View File

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

13
SECURITY.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,22 @@
# Introduction
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
It will walk through the fundamental concepts you need to build applications,
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
It will walk through the fundamental concepts you need to build applications,
beginning with a simple application rendered in the browser, and building toward a
full-stack application with server-side rendering and hydration.
The guide doesnt assume you know anything about fine-grained reactivity or the
details of modern Web frameworks. It does assume you are familiar with the Rust
The guide doesnt assume you know anything about fine-grained reactivity or the
details of modern Web frameworks. It does assume you are familiar with the Rust
programming language, HTML, CSS, and the DOM and basic Web APIs.
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
understand Leptos.
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
**Important Note**: This current version of the book reflects the upcoming `0.5.0` release, which you can install as version `0.5.0-rc3`. 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,13 +23,15 @@ cargo init leptos-tutorial
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos --features=csr,nightly
cargo add leptos@0.5.0-rc3 --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.
Or you can leave off `nightly` if you're using stable Rust
```bash
cargo add leptos --features=csr
cargo add leptos@0.5.0-rc3 --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.
@@ -65,7 +67,7 @@ And add a simple “Hello, world!” to your `main.rs`
use leptos::*;
fn main() {
mount_to_body(|| view! { <p>"Hello, world!"</p> })
mount_to_body(|| view! { <p>"Hello, world!"</p> })
}
```

View File

@@ -367,7 +367,6 @@ fn GlobalStateInput() -> impl IntoView {
// that we created in the other component
// neither of them will cause the other to rerun
let (name, set_name) = create_slice(
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data

View File

@@ -12,6 +12,7 @@
- [Error Handling](./view/07_errors.md)
- [Parent-Child Communication](./view/08_parent_child.md)
- [Passing Children to Components](./view/09_component_children.md)
- [No Macros: The View Builder Syntax](./view/builder.md)
- [Reactivity](./reactivity/README.md)
- [Working with Signals](./reactivity/working_with_signals.md)
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
@@ -46,3 +47,4 @@
- [Deployment](./deployment.md)
- [Appendix: How Does the Reactive System Work?](./appendix_reactive_graph.md)
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)
- [Appendix: Some Small DX Improvements](./appendix_dx.md)

View File

@@ -0,0 +1,49 @@
# A Running List of Small Developer Experience Improvements
## Autocompletion inside `#[component]` and `#[server]`
Because of the nature of macros (they can expand from anything to anything, but only if the input is exactly correct at that instant) it can be hard for rust-analyzer to do proper autocompletion and other support.
But you can tell rust-analyzer to ignore certain proc macros. For `#[component]` and `#[server]` especially, which annotate function bodies but don't actually transform anything inside the body of your function, this can be really helpful.
Note that this means that rust-analyzer doesn't know about your component props, which may generate its own set of errors or warnings in the IDE.
VSCode `settings.json`:
```json
"rust-analyzer.procMacro.ignored": {
"leptos_macro": [
"server",
"component"
],
}
```
neovim with lspconfig:
```lua
require('lspconfig').rust_analyzer.setup {
-- Other Configs ...
settings = {
["rust-analyzer"] = {
-- Other Settings ...
procMacro = {
ignored = {
leptos_macro = {
"server",
"component",
},
},
},
},
}
}
```
Helix, in `.helix/languages.toml`:
```toml
[[language]]
name = "rust"
config = { procMacro = {ignored = {leptos_macro = ["component"]}}}
```

View File

@@ -235,9 +235,9 @@ At the very most, you might consider memoizing the final node before running som
```rust
let text = create_memo(move |_| {
d()
d()
});
create_effect(move |_| {
engrave_text_into_bar_of_gold(&text());
engrave_text_into_bar_of_gold(&text());
});
```

View File

@@ -18,7 +18,7 @@ let async_data = create_resource(
count,
// every time `count` changes, this will run
|value| async move {
log!("loading data from API");
logging::log!("loading data from API");
load_data(value).await
},
);

View File

@@ -4,7 +4,7 @@ In the previous chapter, we showed how you can create a simple loading screen to
```rust
let (count, set_count) = create_signal(0);
let a = create_resource(count, |count| async move { load_a(count).await });
let once = create_resource(count, |count| async move { load_a(count).await });
view! {
<h1>"My Data"</h1>
@@ -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>

View File

@@ -54,7 +54,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_website /app/
COPY --from=builder /app/target/server/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
@@ -68,7 +68,7 @@ ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
# Run the server
CMD ["/app/leptos_website"]
CMD ["/app/leptos_start"]
```
> Read more: [`gnu` and `musl` build files for Leptos apps](https://github.com/leptos-rs/leptos/issues/1152#issuecomment-1634916088).

View File

@@ -50,7 +50,6 @@ If you want to really understand the issue here, it may help to look at the expa
```rust
Suspense(
::leptos::component_props_builder(&Suspense)
.fallback(|| ())
.children({
@@ -61,7 +60,6 @@ Suspense(
leptos::Fragment::lazy(|| {
vec![
(Show(
::leptos::component_props_builder(&Show)
.when(|| true)
// but fallback is moved into Show here
@@ -91,7 +89,7 @@ We can solve this problem by using the [`store_value`](https://docs.rs/leptos/la
In this case, its really simple:
```rust
pub fn LoggedIn<F, IV>(F, children: ChildrenFn) -> impl IntoView
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn() -> IV + 'static,
IV: IntoView,

View File

@@ -30,13 +30,13 @@ Theres a very simple way to determine whether you should use a capital-S `<Sc
There are even a couple elements designed to make semantic HTML and styling easier. [`<Html/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) lets you set the `lang` and `dir` on your `<html>` tag from your application code. `<Html/>` and [`<Body/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Html.html) both have `class` props that let you set their respective `class` attributes, which is sometimes needed by CSS frameworks for styling.
`<Body/>` and `<Html/>` both also have `attributes` props which can be used to set any number of additional attributes on them via the [`AdditionalAttributes`](https://docs.rs/leptos/latest/leptos/struct.AdditionalAttributes.html) type:
`<Body/>` and `<Html/>` both also have `attributes` props which can be used to set any number of additional attributes on them via the `attr:` syntax:
```rust
<Html
lang="he"
dir="rtl"
attributes=AdditionalAttributes::from(vec![("data-theme", "dark")])
attr:data-theme="dark"
/>
```

View File

@@ -50,7 +50,6 @@ let (use_last, set_use_last) = create_signal(true);
// any time one of the source signals changes
create_effect(move |_| {
log(
if use_last() {
format!("{} {}", first(), last())
} else {
@@ -122,7 +121,6 @@ Like `create_resource`, `watch` takes a first argument, which is reactively trac
let (num, set_num) = create_signal(0);
let stop = watch(
move || num.get(),
move |num, prev_num, _| {
log::debug!("Number: {}; Prev: {:?}", num, prev_num);

View File

@@ -20,7 +20,7 @@ let text = move || if count_is_odd() {
// an effect automatically tracks the signals it depends on
// and reruns when they change
create_effect(move |_| {
log!("text = {}", text());
logging::log!("text = {}", text());
});
view! {

View File

@@ -16,7 +16,7 @@ Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `Wr
```rust
let (count, set_count) = create_signal(0);
set_count(1);
log!(count());
logging::log!(count());
```
is the same as
@@ -24,7 +24,7 @@ is the same as
```rust
let (count, set_count) = create_signal(0);
set_count.set(1);
log!(count.get());
logging::log!(count.get());
```
You might notice that `.get()` and `.set()` can be implemented in terms of `.with()` and `.update()`. In other words, `count.get()` is identical with `count.with(|n| n.clone())`, and `count.set(1)` is implemented by doing `count.update(|n| *n = 1)`.
@@ -63,7 +63,35 @@ if names.with(Vec::is_empty) {
}
```
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unncessary closure.
After all, `.with()` simply takes a function that takes the value by reference. Since `Vec::is_empty` takes `&self`, we can pass it in directly and avoid the unnecessary closure.
There are some helper macros to make using `.with()` and `.update()` easier to use, especially when using multiple signals.
```rust
let (first, _) = create_signal("Bob".to_string());
let (middle, _) = create_signal("J.".to_string());
let (last, _) = create_signal("Smith".to_string());
```
If you wanted to concatenate these 3 signals together without unnecessary cloning, you would have to write something like:
```rust
let name = move || {
first.with(|first| {
middle.with(|middle| last.with(|last| format!("{first} {middle} {last}")))
})
};
```
Which is very long and annoying to write.
Instead, you can use the `with!` macro to get references to all the signals at the same time.
```rust
let name = move || with!(|first, middle, last| format!("{first} {middle} {last}"));
```
This expands to the same thing as above. Take a look at the `with!` docs for more info, and the corresponding macros `update!`, `with_value!` and `update_value!`.
## Making signals depend on each other
@@ -80,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

@@ -101,3 +101,44 @@ Now if you navigate to `/` or to `/users` youll get the home page or the `<Us
Note that you can define your routes in any order. The router scores each route to see how good a match it is, rather than simply trying to match them top to bottom.
Simple enough?
## Conditional Routes
`leptos_router` is based on the assumption that you have one and only one `<Routes/>` component in your app. It uses this to generate routes on the server side, optimize route matching by caching calculated branches, and render your application.
You should not conditionally render `<Routes/>` using another component like `<Show/>` or `<Suspense/>`.
```rust
// ❌ don't do this!
view! {
<Show when=|| is_loaded() fallback=|| view! { <p>"Loading"</p> }>
<Routes>
<Route path="/" view=Home/>
</Routes>
</Show>
}
```
Instead, you can use nested routing to render your `<Routes/>` once, and conditionally render the router outlet:
```rust
// ✅ do this instead!
view! {
<Routes>
// parent route
<Route path="/" view=move || {
view! {
// only show the outlet if data have loaded
<Show when=|| is_loaded() fallback=|| view! { <p>"Loading"</p> }>
<Outlet/>
</Show>
}
}>
// nested child route
<Route path="/" view=Home/>
</Route>
</Routes>
}
```
If this looks bizarre, dont worry! The next section of the book is about this kind of nested routing.

View File

@@ -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/>
@@ -153,6 +153,43 @@ pub fn ContactList() -> impl IntoView {
}
```
## Refactoring Route Definitions
You dont need to define all your routes in one place if you dont want to. You can refactor any `<Route/>` and its children out into a separate component.
For example, you can refactor the example above to use two separate components:
```rust
#[component]
fn App() -> impl IntoView {
view! {
<Router>
<Routes>
<Route path="/contacts" view=ContactList>
<ContactInfoRoutes/>
<Route path="" view=|| view! {
<p>"Select a contact to view more info."</p>
}/>
</Route>
</Routes>
</Router>
}
}
#[component(transparent)]
fn ContactInfoRoutes() -> impl IntoView {
view! {
<Route path=":id" view=ContactInfo>
<Route path="" view=EmailAndPhone/>
<Route path="address" view=Address/>
<Route path="messages" view=Messages/>
</Route>
}
}
```
This second component is a `#[component(transparent)]`, meaning it just returns its data, not a view: in this case, it's a [`RouteDefinition`](https://docs.rs/leptos_router/latest/leptos_router/struct.RouteDefinition.html) struct, which is what the `<Route/>` returns. As long as it is marked `#[component(transparent)]`, this sub-route can be defined wherever you want, and inserted as a component into your tree of route definitions.
## Nested Routing and Performance
All of this is nice, conceptually, but again—whats the big deal?

View File

@@ -33,7 +33,6 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
#[component]
pub fn BusyButton() -> impl IntoView {
view! {
<button on:click=move |_| {
spawn_local(async {
add_todo("So much to do!".to_string()).await;
@@ -70,6 +69,18 @@ There are a few things to note about the way you define a server function, too.
- We provide the macro a path. This is a prefix for the path at which well mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
- Youll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
## Server Function URL Prefixes
You can optionally define a specific URL prefix to be used in the definition of the server function.
This is done by providing an optional 2nd argument to the `#[server]` macro.
By default the URL prefix will be `/api`, if not specified.
Here are some examples:
```rust
#[server(AddTodo)] // will use the default URL prefix of `/api`
#[server(AddTodo, "/foo")] // will use the URL prefix of `/foo`
```
## Server Function Encodings
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which well see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
@@ -105,6 +116,21 @@ In other words, you have two choices:
>
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didnt support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
## Server Functions Endpoint Paths
By default, a unique path will be generated. You can optionally define a specific endpoint path to be used in the URL. This is done by providing an optional 4th argument to the `#[server]` macro. Leptos will generate the complete path by concatenating the URL prefix (2nd argument) and the endpoint path (4th argument).
For example,
```rust
#[server(MyServerFnType, "/api", "Url", "hello")]
```
will generate a server function endpoint at `/api/hello` that accepts a POST request.
> **Can I use the same server function endpoint path with multiple encodings?**
>
> No. Different server functions must have unique paths. The `#[server]` macro automatically generates unique paths, but you need to be careful if you choose to specify the complete path manually, as the server looks up server functions by their path.
## An Important Note on Security
Server functions are a cool technology, but its very important to remember. **Server functions are not magic; theyre syntax sugar for defining a public API.** The _body_ of a server function is never made public; its just part of your server binary. But the server function is a publicly accessible API endpoint, and its return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.

View File

@@ -35,7 +35,6 @@ Heres a simplified example from our [`session_auth_axum` example](https://git
```rust
#[server(Login, "/api")]
pub async fn login(
username: String,
password: String,
remember: Option<String>,

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

@@ -9,7 +9,7 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
```rust
#[component]
pub fn App() -> impl IntoView {
leptos::log!("where do I run?");
logging::log!("where do I run?");
// ... whatever
}
```
@@ -87,43 +87,6 @@ The WASM version of your app, running in the browser, expects to find three item
Its pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If youre seeing warnings like this and you dont think its your fault, its much more likely that its a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
### Mutating the DOM during rendering
This is a slightly more common way to create a client/server mismatch: updating a signal _during rendering_ in a way that mutates the view.
```rust
#[component]
pub fn App() -> impl IntoView {
let (loaded, set_loaded) = create_signal(false);
// create_effect only runs on the client
create_effect(move |_| {
// do something like reading from localStorage
set_loaded(true);
});
move || {
if loaded() {
view! { <p>"Hello, world!"</p> }.into_any()
} else {
view! { <div class="loading">"Loading..."</div> }.into_any()
}
}
}
```
This one gives us the scary panic
```
panicked at 'assertion failed: `(left == right)`
left: `"DIV"`,
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
```
And a handy link to this page!
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
#### Solution
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
@@ -166,7 +129,7 @@ For example, say that I want to store something in the browsers `localStorage
pub fn App() -> impl IntoView {
use gloo_storage::Storage;
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
logging::log!("{storage:?}");
}
```
@@ -180,7 +143,7 @@ pub fn App() -> impl IntoView {
use gloo_storage::Storage;
create_effect(move |_| {
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
logging::log!("{storage:?}");
});
}
```

View File

@@ -37,7 +37,7 @@ impl Todos {
#[cfg(test)]
mod tests {
#[test]
fn test_remaining {
fn test_remaining() {
// ...
}
}

View File

@@ -145,7 +145,7 @@ 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 per place we access `double_count`; in other words, twice. This is a
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.

View File

@@ -35,9 +35,7 @@ Instead, lets create a `<ProgressBar/>` component.
```rust
#[component]
fn ProgressBar(
) -> impl IntoView {
fn ProgressBar() -> impl IntoView {
view! {
<progress
max="50"
@@ -64,7 +62,6 @@ In Leptos, you define props by giving additional arguments to the component func
```rust
#[component]
fn ProgressBar(
progress: ReadSignal<i32>
) -> impl IntoView {
view! {
@@ -118,7 +115,6 @@ argument to the component function with `#[prop(optional)]`.
```rust
#[component]
fn ProgressBar(
// mark this prop optional
// you can specify it or not when you use <ProgressBar/>
#[prop(optional)]
@@ -149,7 +145,6 @@ with `#[prop(default = ...)`.
```rust
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
progress: ReadSignal<i32>
@@ -199,7 +194,6 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
```rust
#[component]
fn ProgressBar<F>(
#[prop(default = 100)]
max: u16,
progress: F
@@ -254,7 +248,6 @@ reactive value.
```rust
#[component]
fn ProgressBar(
#[prop(default = 100)]
max: u16,
#[prop(into)]
@@ -373,7 +366,6 @@ component function, and each one of the props:
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
/// The maximum value of the progress bar.
#[prop(default = 100)]
max: u16,

View File

@@ -148,10 +148,10 @@ This _works_, for sure. But if you added a log, you might be surprised
```rust
let message = move || if value() > 5 {
log!("{}: rendering Big", value());
logging::log!("{}: rendering Big", value());
"Big"
} else {
log!("{}: rendering Small", value());
logging::log!("{}: rendering Small", value());
"Small"
};
```

View File

@@ -72,10 +72,7 @@ pub fn App() -> impl IntoView {
#[component]
pub fn ButtonB<F>(
on_click: F,
) -> impl IntoView
pub fn ButtonB<F>(on_click: F) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
{

View File

@@ -47,7 +47,6 @@ Lets define a component that takes some children and a render prop.
```rust
#[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,

View File

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

View File

@@ -41,7 +41,7 @@ workspace = false
description = "Generate the list of workspace members"
script = '''
examples=$(ls |
grep -v README.md |
grep -v .md |
grep -v Makefile.toml |
grep -v cargo-make |
grep -v gtk |
@@ -49,10 +49,12 @@ jq -R -s -c 'split("\n")[:-1]')
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
'''
[tasks.test-runner-report]
[tasks.test-report]
workspace = false
description = "report ci test runners for each example - OPTION: [all]"
description = "report web testing technology used by examples - OPTION: [all]"
script = '''
set -emu
BOLD="\e[1m"
GREEN="\e[0;32m"
ITALIC="\e[3m"
@@ -60,11 +62,10 @@ YELLOW="\e[0;33m"
RESET="\e[0m"
echo
echo "${YELLOW}Test Runner Report${RESET}"
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
echo "${YELLOW}Web Test Technology${RESET}"
echo
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' -not -path '*/node_modules/*' |
sed 's%./%%' |
sed 's%/Makefile.toml%%' |
grep -v Makefile.toml |
@@ -75,38 +76,78 @@ start_path=$(pwd)
for path in $makefile_paths; do
cd $path
test_runner=
crate_symbols=
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
if [ $test_count -gt 0 ]; then
test_runner="-C"
fi
pw_count=$(find . -name playwright.config.ts | wc -l)
while read -r line; do
case $line in
*"cucumber"*)
crate_symbols=$crate_symbols"C"
;;
*"fantoccini"*)
crate_symbols=$crate_symbols"D"
;;
esac
done <"./Cargo.toml"
while read -r line; do
case $line in
*"wasm-test.toml"*)
test_runner=$test_runner"-W"
*"cargo-make/wasm-test.toml"*)
crate_symbols=$crate_symbols"W"
;;
*"playwright-test.toml"*)
test_runner=$test_runner"-P"
*"cargo-make/playwright-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"N"
;;
*"cargo-leptos-test.toml"*)
test_runner=$test_runner"-L"
*"cargo-make/playwright-trunk-test.toml"*)
crate_symbols=$crate_symbols"P"
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/trunk_server.toml"*)
crate_symbols=$crate_symbols"T"
;;
*"cargo-make/cargo-leptos-webdriver-test.toml"*)
crate_symbols=$crate_symbols"L"
;;
*"cargo-make/cargo-leptos-test.toml"*)
crate_symbols=$crate_symbols"L"
if [ $pw_count -gt 0 ]; then
crate_symbols=$crate_symbols"P"
fi
;;
esac
done <"./Makefile.toml"
if [ ! -z "$1" ]; then
# Sort list of tools
sorted_crate_symbols=$(echo ${crate_symbols} | grep -o . | sort | tr -d "\n")
formatted_crate_symbols="${BOLD}${YELLOW}${sorted_crate_symbols}${RESET}"
crate_line=$path
if [ ! -z ${1+x} ]; then
# Show all examples
echo "$path ${BOLD}${test_runner}${RESET}"
elif [ ! -z $test_runner ]; then
if [ ! -z $crate_symbols ]; then
crate_line=$crate_line$formatted_crate_symbols
fi
echo $crate_line
elif [ ! -z $crate_symbols ]; then
# Filter out examples that do not run tests in `ci`
echo "$path ${BOLD}${test_runner}${RESET}"
crate_line=$crate_line$formatted_crate_symbols
echo $crate_line
fi
cd ${start_path}
done
c="${BOLD}${YELLOW}C${RESET} = Cucumber"
d="${BOLD}${YELLOW}D${RESET} = WebDriver"
l="${BOLD}${YELLOW}L${RESET} = Cargo Leptos"
n="${BOLD}${YELLOW}N${RESET} = Node"
p="${BOLD}${YELLOW}P${RESET} = Playwright"
t="${BOLD}${YELLOW}T${RESET} = Trunk"
w="${BOLD}${YELLOW}W${RESET} = WASM"
echo
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
echo
'''

View File

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

68
examples/SSR_NOTES.md Normal file
View File

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

View File

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

View File

@@ -1,9 +1,10 @@
# `<AnimatedShow>` combined with CSS animations
# Animated Show Example
This is a very simple example of the `<AnimatedShow>` component.
This component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the
component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
The `<AnimatedShow>` component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
CSS.
Just execute `trunk serve` to start the demo.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.build]
clear = true
command = "cargo"
@@ -23,33 +27,6 @@ args = ["check-all-features", "--release"]
install_crate = "cargo-all-features"
[tasks.start-client]
dependencies = ["install-cargo-leptos"]
command = "cargo"
args = ["leptos", "watch"]
[tasks.stop-client]
condition = { env_set = ["APP_PROCESS_NAME"] }
script = '''
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
pkill -f todo_app_sqlite
fi
if [ ! -z $(pidof ${APP_PROCESS_NAME}) ]; then
pkill -f cargo-leptos
fi
'''
[tasks.client-status]
condition = { env_set = ["APP_PROCESS_NAME"] }
script = '''
if [ -z $(pidof ${APP_PROCESS_NAME}) ]; then
echo " ${APP_PROCESS_NAME} is not running"
else
echo " ${APP_PROCESS_NAME} is up"
fi
if [ -z $(pidof cargo-leptos) ]; then
echo " cargo-leptos is not running"
else
echo " cargo-leptos is up"
fi
'''

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
extend = [
{ path = "../cargo-make/compile.toml" },
{ path = "../cargo-make/clean.toml" },
{ path = "../cargo-make/lint.toml" },
{ path = "../cargo-make/node.toml" },
{ path = "./compile.toml" },
{ path = "./clean.toml" },
{ path = "./lint.toml" },
{ path = "./node.toml" },
{ path = "./process.toml" },
]
# CI Stages

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,6 @@
This example creates a simple counter in a client side rendered app with Rust and WASM!
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
## Getting Started
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
See the [Examples README](../README.md) for setup and run instructions.

View File

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

View File

@@ -1 +1,8 @@
extend = [{ path = "../cargo-make/main.toml" }]
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "counter_isomorphic"

View File

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

View File

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

View File

@@ -2,6 +2,6 @@
This example creates a simple counter whose state is persisted and synced in the url with query params.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
## Getting Started
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
See the [Examples README](../README.md) for setup and run instructions.

View File

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

View File

@@ -2,6 +2,6 @@
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
## Getting Started
Issue the `cargo make test-flow` command to run unit and wasm tests.
See the [Examples README](../README.md) for setup and run instructions.

View File

@@ -3,9 +3,8 @@ use leptos::{ev, html::*, *};
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(Count::new(initial_value, step));
let count = RwSignal::new(Count::new(initial_value, step));
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
div()
// children can be added with .child()
@@ -18,27 +17,16 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.on(ev::click, move |_| count.update(Count::clear))
.child("Clear"),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
})
.on(ev::click, move |_| count.update(Count::decrease))
.child("-1"),
span()
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child(move || count.get().value())
.child("!"),
))
.child(
span().child(("Value: ", move || count.get().value(), "!")),
button()
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
})
.on(ev::click, move |_| count.update(Count::increase))
.child("+1"),
)
))
}
#[derive(Debug, Clone)]

View File

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

View File

@@ -2,8 +2,6 @@
This example showcases a basic leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events.
## Client Side Rendering
## Getting Started
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
See the [Examples README](../README.md) for setup and run instructions.

View File

@@ -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/>
}
@@ -88,9 +88,17 @@ fn Counter(
set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
// just an example of how a cleanup function works
// this will run when the scope is disposed, i.e., when this row is deleted
on_cleanup(|| log::debug!("deleted a row"));
// because the signal was created in the parent scope, it won't be disposed
// of until the parent scope is. but we no longer need it, so we'll dispose of
// it when this row is deleted, instead. if we don't dispose of it here,
// this memory will "leak," i.e., the signal will continue to exist until the
// parent component is removed. in the case of this component, where it's the
// root, that's the lifetime of the program.
on_cleanup(move || {
log::debug!("deleted a row");
value.dispose();
});
view! {
<li>

View File

@@ -5,12 +5,15 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
leptos_meta = { path = "../../meta", features = ["csr"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2.87"
wasm-bindgen-test = "0.3.37"
pretty_assertions = "1.4.0"
[dev-dependencies.web-sys]
features = [

View File

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

View File

@@ -2,8 +2,6 @@
This example showcases a basic Leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events. Unlike the other counters example, it will compile on Rust stable, because it has the `stable` feature enabled.
## Client Side Rendering
## Getting Started
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
See the [Examples README](../README.md) for setup and run instructions.

View File

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

View File

@@ -0,0 +1,118 @@
use leptos::*;
use leptos_meta::*;
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! {
<Title text="Counters (Stable)" />
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
children=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
// this will run when the scope is disposed, i.e., when this row is deleted
// because the signal was created in the parent scope, it won't be disposed
// of until the parent scope is. but we no longer need it, so we'll dispose of
// it when this row is deleted, instead. if we don't dispose of it here,
// this memory will "leak," i.e., the signal will continue to exist until the
// parent component is removed. in the case of this component, where it's the
// root, that's the lifetime of the program.
on_cleanup(move || {
log::debug!("deleted a row");
value.dispose();
});
view! {
<li>
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input data-testid="counter_input" type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
use wasm_bindgen_test::*;
// Test Suites
pub mod add_1k_counters;
pub mod add_counter;
pub mod clear_counters;
pub mod decrement_counter;
pub mod enter_count;
pub mod increment_counter;
pub mod remove_counter;
pub mod view_counters;
pub mod fixtures;
pub use fixtures::*;
wasm_bindgen_test_configure!(run_in_browser);

View File

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

View File

@@ -0,0 +1,22 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_see_the_initial_counts() {
// When
ui::view_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}
#[wasm_bindgen_test]
fn should_see_the_title() {
// When
ui::view_counters();
// Then
assert_eq!(ui::title(), "Counters (Stable)");
}

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

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

View File

@@ -1 +1,4 @@
extend = [{ path = "../cargo-make/main.toml" }]
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/playwright-trunk-test.toml" },
]

View File

@@ -2,6 +2,10 @@
This example shows how to handle basic errors using Leptos.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
## Getting Started
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
See the [Examples README](../README.md) for setup and run instructions.
## Testing
This project is configured to run start and stop of processes for integration tests wihtout the use of Cargo Leptos or Node.

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Clear Number", () => {
test("should see the error message", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clearInput();
await expect(ui.errorMessage).toHaveText("Not a number! Errors: ");
});
test("should see the error list", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clearInput();
await expect(ui.errorList).toHaveText(
"cannot parse integer from empty string"
);
});
});

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