Compare commits

..

124 Commits

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

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

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

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

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

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

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

* test(ci): simulate leptos change

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

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

* refactor(examples): extract client process tasks

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

* test(error_boundary): click up arrow

* test(error_boundary): click down arrow

* test(error_boundary): type number

* test(error_boundary): clear number

* fix(build): clean trunk directories

* fix(test-report): detect unit tests

* ci(build): echo stop trunk
2023-09-04 13:25:44 -04:00
Greg Johnston
70e1ad41e2 Merge pull request #1579 from leptos-rs/rusty
feat: start adding some Rustier interfaces for reactive types
2023-09-04 13:23:18 -04:00
Greg Johnston
53ec7ed272 feat: add Effect::with_value_mut() 2023-09-04 11:14:07 -04:00
Greg Johnston
d98a577740 feat: add Rustier interfaces for reactive system types 2023-09-04 11:05:23 -04:00
jquesada2016
fd834f48c2 change: rename .derived_signal() and .mapped_signal_setter() methods (#1637) 2023-09-04 08:41:41 -04:00
Greg Johnston
7be65a37c6 fix: versioned resources never decrement Suspense (closes #1640) (#1641) 2023-09-03 20:21:16 -04:00
Banzobotic
3b5e2d86fb docs: clean up messy spacing left over from cx replacements (#1626) 2023-09-03 20:21:05 -04:00
jquesada2016
716b9fb50b feat: add .into_X_boxed() for classes, properties, and styles as for attributes 2023-09-03 20:18:49 -04:00
jquesada2016
006ca13797 chore: hide get_property (#1638) 2023-09-03 20:15:15 -04:00
Village
6e008343c8 feat: add component generics (#1636) 2023-09-03 20:09:50 -04:00
Greg Johnston
2ca24883ac fix: memoize Suspense readiness to avoid rerendering children/fallback (#1642) 2023-09-03 20:07:20 -04:00
Village
4a43983f4e feat: implement spreading attributes onto elements (#1619) 2023-09-01 20:52:15 -04:00
IcosaHedron
d9e83121c1 feat: add reload websocket configuration and enable env configuration (#1613) 2023-09-01 20:51:46 -04:00
Antonin Peronnet
f5b4b97c9b feat: Callback types to make it easier to accept (optional) callback props (#1596) 2023-09-01 20:51:32 -04:00
Gareth
bcfa430a40 docs: fix incorrect variable name (#1623) 2023-09-01 07:39:41 -04:00
Lawrence Qupty
7c51815cf5 docs: remove extra space (#1622) 2023-09-01 07:39:05 -04:00
Dmitry Pytaylo
fee2fb953b docs: fix typo (#1618) 2023-09-01 07:37:52 -04:00
Sadra M
8ecb7f59c4 docs: update references to server binary in dockerfile (#1617) 2023-09-01 07:37:24 -04:00
martin frances
b85cb9fb3b docs: clarify how many times derived signals are called (#1614) 2023-09-01 07:36:15 -04:00
Joseph Cruz
a631c5ca1c doc(examples): report fantoccini use (#1616) 2023-09-01 07:35:29 -04:00
343 changed files with 11512 additions and 4331 deletions

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

@@ -24,7 +24,7 @@ jobs:
steps:
# Setup environment
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1

View File

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

@@ -47,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;

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

@@ -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,9 +49,9 @@ 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
@@ -62,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 |
@@ -77,56 +76,78 @@ start_path=$(pwd)
for path in $makefile_paths; do
cd $path
test_runner=
crate_symbols=
test_count=$(grep -rl -E "#\[test\]" | wc -l)
if [ $test_count -gt 0 ]; then
test_runner="T"
fi
pw_count=$(find . -name playwright.config.ts | wc -l)
while read -r line; do
case $line in
*"cucumber"*)
test_runner=$test_runner"C"
crate_symbols=$crate_symbols"C"
;;
*"rstest"*)
test_runner=$test_runner"R"
*"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"
runners=$(echo ${test_runner} | grep -o . | sort | tr -d "\n")
# 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}${runners}${RESET}"
elif [ ! -z $runners ]; 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}${runners}${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 = Cucumber, L = Cargo Leptos, P = Playwright, R = RS Test, T = Cargo, W = WASM${RESET}"
echo "${ITALIC}Keys:${RESET} $c, $d, $l, $n, $p, $t, $w"
echo
'''
# ALIASES
[tasks.tr]
alias = "test-runner-report"

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

@@ -12,7 +12,7 @@ args = ["-rf", "target"]
[tasks.clean-trunk]
script = '''
find . -type d -name target | xargs rm -rf
find . -type d -name dist | xargs rm -rf
'''
[tasks.clean-node_modules]

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,7 +3,7 @@ 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));
// the function name is the same as the HTML tag name
div()
@@ -17,18 +17,14 @@ 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: ", 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"),
))
}

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

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

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

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"
);
});
});

View File

@@ -0,0 +1,17 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Click Down Arrow", () => {
test("should see the negative number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await ui.clickDownArrow();
await expect(ui.successMessage).toHaveText("You entered -5");
});
});

View File

@@ -0,0 +1,15 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Click Up Arrow", () => {
test("should see the positive number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.clickUpArrow();
await ui.clickUpArrow();
await ui.clickUpArrow();
await expect(ui.successMessage).toHaveText("You entered 3");
});
});

View File

@@ -0,0 +1,56 @@
import { expect, Locator, Page } from "@playwright/test";
export class HomePage {
readonly page: Page;
readonly pageTitle: Locator;
readonly numberInput: Locator;
readonly successMessage: Locator;
readonly errorMessage: Locator;
readonly errorList: Locator;
constructor(page: Page) {
this.page = page;
this.pageTitle = page.locator("h1");
this.numberInput = page.getByLabel(
"Type a number (or something that's not a number!)"
);
this.successMessage = page.locator("label p");
this.errorMessage = page.locator("div p");
this.errorList = page.getByRole("list");
}
async goto() {
await this.page.goto("/");
}
async enterNumber(count: string, index: number = 0) {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.fill(count),
]);
}
async clickUpArrow() {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.press("ArrowUp"),
]);
}
async clickDownArrow() {
await Promise.all([
this.numberInput.waitFor(),
this.numberInput.press("ArrowDown"),
]);
}
async clearInput() {
await Promise.all([
this.numberInput.waitFor(),
this.clickUpArrow(),
this.numberInput.press("Backspace"),
]);
}
}

View File

@@ -0,0 +1,11 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Open App", () => {
test("should see the page title", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await expect(ui.pageTitle).toHaveText("Error Handling");
});
});

View File

@@ -0,0 +1,13 @@
import { test, expect } from "@playwright/test";
import { HomePage } from "./fixtures/home_page";
test.describe("Type Number", () => {
test("should see the typed number", async ({ page }) => {
const ui = new HomePage(page);
await ui.goto();
await ui.enterNumber("7");
await expect(ui.successMessage).toHaveText("You entered 7");
});
});

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 = "errors_axum"

View File

@@ -1,41 +1,7 @@
# Leptos Errors Demonstration with Axum
This example demonstrates how Leptos Errors can work with an Axum backend on a server.
## Client Side Rendering
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send status codes.
## 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 `"."`. You'll 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,6 +1,6 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::{Errors, *};
use leptos::{logging::log, Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@@ -45,7 +45,7 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _)| *index
// renders each item to a view
view=move |error| {
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {

View File

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

View File

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

View File

@@ -2,8 +2,6 @@
This example shows how to fetch data from the client in WebAssembly.
## 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

@@ -41,6 +41,12 @@ ssr = [
"leptos_router/ssr",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
@@ -88,3 +94,5 @@ lib-features = ["hydrate"]
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

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 = "hackernews"

View File

@@ -2,42 +2,6 @@
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository
## Client Side Rendering
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. Make sure you have trunk installed with `cargo install trunk`.
## 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 `"."`. You'll 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

@@ -17,8 +17,7 @@ where
let abort_controller = web_sys::AbortController::new().ok();
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
// abort in-flight requests if, e.g., we've navigated away from this page
leptos::on_cleanup(move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()

View File

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

View File

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

View File

@@ -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 = "hackernews_axum"

View File

@@ -2,42 +2,6 @@
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
## Client Side Rendering
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. Make sure you have trunk installed with `cargo install trunk`.
## 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 `"."`. You'll 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

@@ -17,8 +17,7 @@ where
let abort_controller = web_sys::AbortController::new().ok();
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
// abort in-flight requests if e.g., we've navigated away from this page
leptos::on_cleanup(move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()

View File

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

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