Compare commits

...

82 Commits
1403 ... v0.4.9

Author SHA1 Message Date
Greg Johnston
e9deff52a7 v0.4.9 2023-08-20 14:27:49 -04:00
Greg Johnston
eb3d9b8714 build(docs): only publish on new version tags (#1562) 2023-08-20 09:38:35 -04:00
luoxiaozero
18deb398ca feat: tracing support for component props (#1531) 2023-08-18 08:29:41 -04:00
Geert Stappers
d9abebb4be docs: add link to source code for book 2023-08-18 07:57:38 -04:00
Jonathan
a480db8b77 <Show/> update (#1557) 2023-08-18 07:50:26 -04:00
Greg Johnston
1f26b68d45 docs: inner_html in book 2023-08-16 21:31:06 -04:00
Greg Johnston
937501c61b docs: add note about #[component(transparent)] 2023-08-16 21:26:53 -04:00
Joseph Cruz
5523fb86fb perf(check-stable): only run on source change (#1542) 2023-08-15 06:20:01 -04:00
Joseph Cruz
7dcfcf8ca8 chore(test_examples): remove obsolete directory (#1540) 2023-08-15 06:19:36 -04:00
Joseph Cruz
087c68569a test(suspense-tests): add e2e tests (Closes #1519) (#1533)
* test(suspense-tests): add e2e tests (closes #1519)

test(suspense_tests): load nested

test(suspense_tests): load parallel

test(suspsense_tests): load nested inside

test(suspense_tests): load single

test(suspense_tests): load inside component

test(suspense_tests): load no resources

test(suspense_tests): click nested count

test(suspense_tests): click inside component count

test(suspense_tests): click nested inside count

test(suspense_tests): click single count

test(suspense_tests): click parallel counts

test(suspense_tests): click no resources count

refactor(suspense_tests): change view strategy

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

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

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

* build(examples): set toolchain for compiles tasks

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

* build(examples): set toolchain of other examples
2023-07-24 11:35:34 -04:00
Greg Johnston
984a7388f1 fix: clear <title> correctly when navigating between pages (closes #1369) (#1428) 2023-07-24 11:25:28 -04:00
Greg Johnston
274b105676 docs: fix messed up component closing tags router docs 2023-07-24 11:24:58 -04:00
Greg Johnston
a689d1b4c0 docs: note on optional generic component props 2023-07-24 07:52:40 -04:00
Greg Johnston
1581e91317 docs: note on how to opt out of client-side routing 2023-07-24 07:52:29 -04:00
Andrew Grande
20f4034c1c docs: proofreading and fixing the links (#1425)
* Update 23_ssr_modes.md

Fixed grammar, added the section anchor links

* Fixed a broken link

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

* Update 26_extractors.md

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

* build(examples): add show all mode
2023-07-22 08:13:49 -04:00
Greg Johnston
62bf315059 fix: <use_/> as typed top-level element in view (#1410) 2023-07-21 10:07:34 -04:00
Greg Johnston
011c97e3a4 fix: closing element names wrong for svg::, math::, and use_ (closes #1403) (#1406) 2023-07-20 17:03:30 -04:00
Greg Johnston
2ca3d2c7a4 fix: RawText/unquoted text nodes in SSR (closes #1384) (#1407) 2023-07-20 17:03:19 -04:00
Greg Johnston
cc52c94348 docs/examples: use shorthand form for <Route/> views when possible (#1375) 2023-07-20 16:28:43 -04:00
Andrew Grande
4b8cc96dfa docs: typo in 16_routes.md
Fixed grammar
2023-07-20 16:13:25 -04:00
225 changed files with 13817 additions and 3236 deletions

View File

@@ -11,8 +11,45 @@ env:
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
setup:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
test:
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
runs-on: ${{ matrix.os }}
strategy:
matrix:

View File

@@ -1,9 +1,8 @@
name: Deploy book
on:
push:
paths: ['docs/book/**']
branches:
- main
tags:
- '*-?v[0-9]+*'
jobs:
deploy:
@@ -34,4 +33,4 @@ jobs:
mv ../book/* .
git add .
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
git push --force --set-upstream origin gh-pages
git push --force --set-upstream origin gh-pages

View File

@@ -84,6 +84,11 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install Chrome Webriver
run: |
sudo apt-get update
sudo apt-get install chromium-chromedriver
# Run Cargo Make Task
- name: ${{ inputs.cargo_make_task }}
run: |

View File

@@ -70,6 +70,25 @@ are a few guidelines that will make it a better experience for everyone:
`cargo-make` and using `cargo make check && cargo make test && cargo make
check-examples`.
## Before Submitting a PR
We have a fairly extensive CI setup that runs both lints (like `rustfmt` and `clippy`)
and tests on PRs. You can run most of these locally if you have `cargo-make` installed.
If you added an example, make sure to add it to the list in `examples/Makefile.toml`.
From the root directory of the repo, run
- `cargo +nightly fmt`
- `cargo +nightly make check`
- `cargo +nightly make test`
- `cargo +nightly make check-examples`
- `cargo +nightly make --profile=github-actions ci`
If you modified an example:
- `cd examples/your_example`
- `cargo +nightly fmt -- --config-path ../..`
- `cargo +nightly make --profile=github-actions verify-flow`
## Architecture
See [ARCHITECTURE.md](./ARCHITECTURE.md).

View File

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

View File

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

View File

@@ -88,8 +88,6 @@ targets = ["wasm32-unknown-unknown"]
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
> Note: The `nightly` feature is present on the main branch version right now, but not in 0.3.x. For 0.3.x, nightly is the default and `stable` has a special feature.
## `cargo-leptos`
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).

View File

@@ -17,4 +17,4 @@ understand Leptos.
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
**The guide is a work in progress.**
> The source code for the book is available [here](https://github.com/leptos-rs/leptos/tree/main/docs/book). PRs for typos or clarification are always welcome.

View File

@@ -26,7 +26,7 @@ cargo init leptos-tutorial
cargo add leptos --features=csr,nightly
```
Or you can leave off `nighly` if you're using stable Rust
Or you can leave off `nightly` if you're using stable Rust
```bash
cargo add leptos --features=csr
```

View File

@@ -43,5 +43,6 @@
- [Responses and Redirects](./server/27_response.md)
- [Progressive Enhancement and Graceful Degradation](./progressive_enhancement/README.md)
- [`<ActionForm/>`s](./progressive_enhancement/action_form.md)
- [Deployment]()
- [Deployment](./deployment.md)
- [Appendix: How Does the Reactive System Work?](./appendix_reactive_graph.md)
- [Appendix: Optimizing WASM Binary Size](./appendix_binary_size.md)

View File

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

View File

@@ -69,6 +69,34 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
This inversion of the flow of control makes it easier to add or remove individual resources, as you dont need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which well talk about during a later chapter.
## `<Await/>`
In youre simply trying to wait for some `Future` to resolve before rendering, you may find the `<Await/>` component helpful in reducing boilerplate. `<Await/>` essentially combines a resource with the source argument `|| ()` with a `<Suspense/>` with no fallback.
In other words:
1. It only polls the `Future` once, and does not respond to any reactive changes.
2. It does not render anything until the `Future` resolves.
3. After the `Future` resolves, its binds its data to whatever variable name you choose and then renders its children with that variable in scope.
```rust
async fn fetch_monkeys(monkey: i32) -> i32 {
// maybe this didn't need to be async
monkey * 2
}
view! { cx,
<Await
// `future` provides the `Future` to be resolved
future=|cx| fetch_monkeys(3)
// the data is bound to whatever variable name you provide
bind:data
>
// you receive the data by reference and can use it in your view here
<p>{*data} " little monkeys, jumping on the bed."</p>
</Await>
}
```
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

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

View File

@@ -109,6 +109,34 @@ create_effect(cx, move |prev_value| {
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
## Explicit, Cancelable Tracking with `watch`
In addition to `create_effect`, Leptos provides a [`watch`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.watch.html) function, which can be used for two main purposes:
1. Separating tracking and responding to changes by explicitly passing in a set of values to track.
2. Canceling tracking by calling a stop function.
Like `create_resource`, `watch` takes a first argument, which is reactively tracked, and a second, which is not. Whenever a reactive value in its `deps` argument is changed, the `callback` is run. `watch` returns a function that can be called to stop tracking the dependencies.
```rust
let (num, set_num) = create_signal(cx, 0);
let stop = watch(
cx,
move || num.get(),
move |num, prev_num, _| {
log::debug!("Number: {}; Prev: {:?}", num, prev_num);
},
false,
);
set_num.set(1); // > "Number: 1; Prev: Some(0)"
stop(); // stop watching
set_num.set(2); // (nothing happens)
```
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -24,7 +24,7 @@ use leptos_router::*;
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
> You shouldnt try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
> You shouldnt try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which one decides what to do when the URL changes?
Lets start with a simple `<App/>` component using the router:
@@ -87,15 +87,17 @@ The `view` is a function that takes a `Scope` and returns a view.
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home/> }/>
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
<Route path="/" view=Home/>
<Route path="/users" view=Users/>
<Route path="/users/:id" view=UserProfile/>
<Route path="/*any" view=NotFound/>
</Routes>
```
> The router scores each route to see how good a match it is, so you can define your routes in any order.
> `view` takes a `Fn(Scope) -> impl IntoView`. If a component has no props, it is a function that takes `Scope` and returns `impl IntoView`, so it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|cx| view! { cx, <Home/> }`.
Now if you navigate to `/` or to `/users` youll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` youll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
Note that you can define your routes in any order. The router scores each route to see how good a match it is, rather than simply trying to match them top to bottom.
Simple enough?

View File

@@ -4,10 +4,10 @@ We just defined the following set of routes:
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home /> }/>
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
<Route path="/" view=Home/>
<Route path="/users" view=Users/>
<Route path="/users/:id" view=UserProfile/>
<Route path="/*any" view=NotFound/>
</Routes>
```
@@ -17,11 +17,11 @@ Well... you can!
```rust
<Routes>
<Route path="/" view=|cx| view! { cx, <Home /> }/>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="/" view=Home/>
<Route path="/users" view=Users>
<Route path=":id" view=UserProfile/>
</Route>
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
<Route path="/*any" view=NotFound/>
</Routes>
```
@@ -39,8 +39,8 @@ Lets look back at our practical example.
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="/users" view=Users/>
<Route path="/users/:id" view=UserProfile/>
</Routes>
```
@@ -53,8 +53,8 @@ Lets say I use nested routes instead:
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="/users" view=Users>
<Route path=":id" view=UserProfile/>
</Route>
</Routes>
```
@@ -68,9 +68,9 @@ I actually need to add a fallback route
```rust
<Routes>
<Route path="/users" view=|cx| view! { cx, <Users /> }>
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
<Route path="" view=|cx| view! { cx, <NoUser /> }/>
<Route path="/users" view=Users>
<Route path=":id" view=UserProfile/>
<Route path="" view=NoUser/>
</Route>
</Routes>
```
@@ -94,8 +94,8 @@ You can easily define this with nested routes
```rust
<Routes>
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }/>
<Route path="/contacts" view=ContactList>
<Route path=":id" view=ContactInfo/>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
}/>
@@ -107,11 +107,11 @@ You can go even deeper. Say you want to have tabs for each contacts address,
```rust
<Routes>
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
<Route path="address" view=|cx| view! { cx, <Address/> }/>
<Route path="messages" view=|cx| view! { cx, <Messages/> }/>
<Route path="/contacts" view=ContactList>
<Route path=":id" view=ContactInfo>
<Route path="" view=EmailAndPhone/>
<Route path="address" view=Address/>
<Route path="messages" view=Messages/>
</Route>
<Route path="" view=|cx| view! { cx,
<p>"Select a contact to view more info."</p>
@@ -201,12 +201,9 @@ fn App(cx: Scope) -> impl IntoView {
// /contacts has nested routes
<Route
path="/contacts"
view=|cx| view! { cx, <ContactList/> }
>
view=ContactList
// if no id specified, fall back
<Route path=":id" view=|cx| view! { cx,
<ContactInfo/>
}>
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"

View File

@@ -36,6 +36,14 @@ struct ContactSearch {
```
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure youre importing the right one for the derive macro.
>
> If you are not using the `nightly` feature, you will get the error
>
> ```
> no function or associated item named `into_param` found for struct `std::string::String` in the current scope
> ```
>
> At the moment, supporting both `T: FromStr` and `Option<T>` for typed params requires a nightly feature. You can fix this by simply changing the struct to use `q: Option<String>` instead of `q: String`.
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
@@ -108,12 +116,10 @@ fn App(cx: Scope) -> impl IntoView {
// /contacts has nested routes
<Route
path="/contacts"
view=|cx| view! { cx, <ContactList/> }
view=ContactList
>
// if no id specified, fall back
<Route path=":id" view=|cx| view! { cx,
<ContactInfo/>
}>
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"

View File

@@ -11,11 +11,27 @@ The router will bail out of handling an `<a>` click under a number of situations
In other words, the router will only try to do a client-side navigation when its pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
> This also means that if you need to opt out of client-side routing, you can do so easily. For example, if you have a link to another page on the same domain, but which isnt part of your Leptos app, you can just use `<a rel="external">` to tell the router it isnt something it can handle.
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., its a link to the page youre on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if its a link to the page youre currently on, you can match this attribute with a CSS selector.
## Navigating Programmatically
Your most-used methods of navigating between pages should be with `<a>` and `<form>` elements or with the enhanced `<A/>` and `<Form/>` components. Using links and forms to navigate is the best solution for accessibility and graceful degradation.
On occasion, though, youll want to navigate programmatically, i.e., call a function that can navigate to a new page. In that case, you should use the [`use_navigate`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_navigate.html) function.
```rust
let navigate = leptos_router::use_navigate(cx);
navigate("/somewhere", Default::default());
```
> You should almost never do something like `<button on:click=move |_| navigate(/* ... */)>`. Any `on:click` that navigates should be an `<a>`, for reasons of accessibility.
The second argument here is a set of [`NavigateOptions`](https://docs.rs/leptos_router/latest/leptos_router/struct.NavigateOptions.html), which includes options to resolve the navigation relative to the current route as the `<A/>` component does, replace it in the navigation stack, include some navigation state, and maintain the current scroll state on navigation.
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
@@ -52,12 +68,10 @@ fn App(cx: Scope) -> impl IntoView {
// /contacts has nested routes
<Route
path="/contacts"
view=|cx| view! { cx, <ContactList/> }
view=ContactList
>
// if no id specified, fall back
<Route path=":id" view=|cx| view! { cx,
<ContactInfo/>
}>
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"

View File

@@ -80,7 +80,7 @@ fn App(cx: Scope) -> impl IntoView {
<h1><code>"<Form/>"</code></h1>
<main>
<Routes>
<Route path="" view=|cx| view! { cx, <FormExample/> }/>
<Route path="" view=FormExample/>
</Routes>
</main>
</Router>

View File

@@ -113,7 +113,7 @@ Server functions are a cool technology, but its very important to remember. *
So far, everything Ive said is actually framework agnostic. (And in fact, the Leptos server function crate has been integrated into Dioxus as well!) Server functions are simply a way of defining a function-like RPC call that leans on Web standards like HTTP requests and URL encoding.
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](../async/README.md). So you can easily integrate your server functions with the rest of your applications:
But in a way, they also provide the last missing primitive in our story so far. Because a server function is just a plain Rust async function, it integrates perfectly with the async Leptos primitives we discussed [earlier](https://leptos-rs.github.io/leptos/async/index.html). So you can easily integrate your server functions with the rest of your applications:
- Create **resources** that call the server function to load data from the server
- Read these resources under `<Suspense/>` or `<Transition/>` to enable streaming SSR and fallback states while data loads.

View File

@@ -6,9 +6,9 @@ The server functions we looked at in the last chapter showed how to run code on
We call Leptos a “full-stack” framework, but “full-stack” is always a misnomer (after all, it never means everything from the browser to your power company.) For us, “full stack” means that your Leptos app can run in the browser, and can run on the server, and can integrate the two, drawing together the unique features available in each; as weve seen in the book so far, a button click on the browser can drive a database read on the server, both written in the same Rust module. But Leptos itself doesnt provide the server (or the database, or the operating system, or the firmware, or the electrical cables...)
Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_actix/latest/leptos_axum/)). Weve built integrations with each servers router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
Instead, Leptos provides integrations for the two most popular Rust web server frameworks, Actix Web ([`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/)) and Axum ([`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/)). Weve built integrations with each servers router so that you can simply plug your Leptos app into an existing server with `.leptos_routes()`, and easily handle server function calls.
> If havent seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, nows a good time to check them out.
> If you havent seen our [Actix](https://github.com/leptos-rs/start) and [Axum](https://github.com/leptos-rs/start-axum) templates, nows a good time to check them out.
## Using Extractors
@@ -43,7 +43,7 @@ pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
## Axum Extractors
The syntax for the `leptos_axum::extract` function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so youll need to add something to handle the error case.
The syntax for the [`leptos_axum::extract`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract.html) function is very similar. (**Note**: This is available on the git main branch, but has not been released as of writing.) Note that Axum extractors return a `Result`, so youll need to add something to handle the error case.
```rust
#[server(AxumExtract, "/api")]
@@ -62,7 +62,21 @@ pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
These are relatively simple examples accessing basic data from the server. But you can use extractors to access things like headers, cookies, database connection pools, and more, using the exact same `extract()` pattern.
> Note: For now, the Axum `extract` function only supports extractors for which the state is `()`, i.e., you can't yet use it to extract `State(_)`. You can access `State(_)` by using a custom handler that extracts the state and then provides it via context. [Click here for an example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/main.rs#L91-L92).
The Axum `extract` function only supports extractors for which the state is `()`. If you need an extractor that uses `State`, you should use [`extract_with_state`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.extract_with_state.html). This requires you to provide the state. You can do this by extending the existing `LeptosOptions` state using the Axum `FromRef` pattern, which providing the state as context during render and server functions with custom handlers.
```rust
use axum::extract::FromRef;
/// Derive FromRef to allow multiple items in state, using Axums
/// SubStates pattern.
#[derive(FromRef, Debug, Clone)]
pub struct AppState{
pub leptos_options: LeptosOptions,
pub pool: SqlitePool
}
```
[Click here for an example of providing context in custom handlers](https://github.com/leptos-rs/leptos/blob/19ea6fae6aec2a493d79cc86612622d219e6eebb/examples/session_auth_axum/src/main.rs#L24-L44).
## A Note about Data-Loading Patterns

View File

@@ -8,7 +8,12 @@ If youve ever listened to streaming music or watched a video online, Im su
Let me say a little more about what I mean.
Leptos supports all four different of these different ways to render HTML that includes asynchronous data.
Leptos supports all four different modes of rendering HTML that includes asynchronous data:
1. [Synchronous Rendering](#synchronous-rendering)
1. [Async Rendering](#async-rendering)
1. [In-Order streaming](#in-order-streaming)
1. [Out-of-Order Streaming](#out-of-order-streaming)
## Synchronous Rendering
@@ -64,7 +69,7 @@ If youre using server-side rendering, the synchronous mode is almost never wh
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. If one of them reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order as usual.
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is *not* useful if theres only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is _not_ useful if theres only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
- _Pros_: Works if JavaScript is disabled or not supported on the users device.
- _Cons_
@@ -79,13 +84,13 @@ Because it offers the best blend of performance characteristics, Leptos defaults
```rust
<Routes>
// Well load the home page with out-of-order streaming and <Suspense/>
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
<Route path="" view=HomePage/>
// We'll load the posts with async rendering, so they can set
// the title and metadata *after* loading the data
<Route
path="/post/:id"
view=|cx| view! { cx, <Post/> }
view=Post
ssr=SsrMode::Async
/>
</Routes>

View File

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

View File

@@ -139,7 +139,7 @@ view! { cx,
Remember—and this is _very important_—only functions are reactive. This means that
`{count}` and `{count()}` do very different things in your view. `{count}` passes
in a function, telling the framework to update the view every time `count` changes.
`{count()}` access the value of `count` once, and passes an `i32` into the view,
`{count()}` accesses the value of `count` once, and passes an `i32` into the view,
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
Lets make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Lets replace “set this value to 3” with “increment this value by 1”:

View File

@@ -144,10 +144,27 @@ let double_count = move || count() * 2;
Derived signals let you create reactive computed values that can be used in multiple
places in your application with minimal overhead.
> Note: Using a derived signal like this means that the calculation runs once per
> signal change per place we access `double_count`; in other words, twice. This is a
> very cheap calculation, so thats fine. Well look at memos in a later chapter, which
> are designed to solve this problem for expensive calculations.
Note: Using a derived signal like this means that the calculation runs once per
signal change 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.
> #### Advanced Topic: Injecting Raw HTML
>
> The `view` macro provides support for an additional attribute, `inner_html`, which
> can be used to directly set the HTML contents of any element, wiping out any other
> children youve given it. Note that this does _not_ escape the HTML you provide. You
> should make sure that it only contains trusted input or that any HTML entities are
> escaped, to prevent cross-site scripting (XSS) attacks.
>
> ```rust
> let html = "<p>This HTML will be injected.</p>";
> view! { cx,
> <div inner_html=html/>
> }
> ```
>
> [Click here for the full `view` macros docs](https://docs.rs/leptos/latest/leptos/macro.view.html).
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)

View File

@@ -219,9 +219,25 @@ where
This is a perfectly reasonable way to write this component: `progress` now takes
any value that implements this `Fn()` trait.
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
> or as `progress: impl Fn() -> i32 + 'static,`, in part because theyre actually used to generate
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
This generic can also be specified inline:
```rust
#[component]
fn ProgressBar<F: Fn() -> i32 + 'static>(
cx: Scope,
#[prop(default = 100)] max: u16,
progress: F,
) -> impl IntoView {
view! { cx,
<progress
max=max
value=progress
/>
}
}
```
> Note that generic component props _cant_ be specified with an `impl` yet (`progress: impl Fn() -> i32 + 'static,`), in part because theyre actually used to generate a `struct ProgressBarProps`, and struct fields cannot be `impl` types. The `#[component]` macro may be further improved in the future to allow inline `impl` generic props.
### `into` Props
@@ -271,6 +287,81 @@ fn App(cx: Scope) -> impl IntoView {
}
```
### Optional Generic Props
Note that you cant specify optional generic props for a component. Lets see what would happen if you try:
```rust,compile_fail
#[component]
fn ProgressBar<F: Fn() -> i32 + 'static>(
cx: Scope,
#[prop(optional)] progress: Option<F>,
) -> impl IntoView {
progress.map(|progress| {
view! { cx,
<progress
max=100
value=progress
/>
}
})
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
<ProgressBar/>
}
}
```
Rust helpfully gives the error
```
xx | <ProgressBar/>
| ^^^^^^^^^^^ cannot infer type of the type parameter `F` declared on the function `ProgressBar`
|
help: consider specifying the generic argument
|
xx | <ProgressBar::<F>/>
| +++++
```
There are just two problems:
1. Leptoss view macro doesnt support specifying a generic on a component with this turbofish syntax.
2. Even if you could, specifying the correct type here is not possible; closures and functions in general are unnameable types. The compiler can display them with a shorthand, but you cant specify them.
However, you can get around this by providing a concrete type using `Box<dyn _>` or `&dyn _`:
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>,
) -> impl IntoView {
progress.map(|progress| {
view! { cx,
<progress
max=100
value=progress
/>
}
})
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
<ProgressBar/>
}
}
```
Because the Rust compiler now knows the concrete type of the prop, and therefore its size in memory even in the `None` case, this compiles fine.
> In this particular case, `&dyn Fn() -> i32` will cause lifetime issues, but in other cases, it may be a possibility.
## Documenting Components
This is one of the least essential but most important sections of this book.
@@ -306,6 +397,24 @@ type, and each of the fields used to add props. It can be a little hard to
understand how powerful this is until you hover over the component name or props
and see the power of the `#[component]` macro combined with rust-analyzer here.
> #### Advanced Topic: `#[component(transparent)]`
>
> All Leptos components return `-> impl IntoView`. Some, though, need to return
> some data directly without any additional wrapping. These can be marked with
> `#[component(transparent)]`, in which case they return exactly the value they
> return, without the rendering system transforming them in any way.
>
> This is mostly used in two situations:
>
> 1. Creating wrappers around `<Suspense/>` or `<Transition/>`, which return a
> transparent suspense structure to integrate with SSR and hydration properly.
> 2. Refactoring `<Route/>` definitions for `leptos_router` out into separate
> components, because `<Route/>` is a transparent component that returns a
> `RouteDefinition` struct rather than a view.
>
> In general, you should not need to use transparent components unless you are
> creating custom wrapping components that fall into one of these two categories.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>

View File

@@ -19,7 +19,8 @@ There are two important things to remember:
2. The `value` _attribute_ only sets the initial value of the input, i.e., it
only updates the input up to the point that you begin typing. The `value`
_property_ continues updating the input after that. You usually want to set
`prop:value` for this reason.
`prop:value` for this reason. (The same is true for `checked` and `prop:checked`
on an `<input type="checkbox">`.)
```rust
let (name, set_name) = create_signal(cx, "Controlled".to_string());
@@ -42,6 +43,33 @@ view! { cx,
}
```
> #### Why do you need `prop:value`?
>
> Web browsers are the most ubiquitous and stable platform for rendering graphical user interfaces in existence. They have also maintained an incredible backwards compatibility over their three decades of existence. Inevitably, this means there are some quirks.
>
> One odd quirk is that there is a distinction between HTML attributes and DOM element properties, i.e., between something called an “attribute” which is parsed from HTML and can be set on a DOM element with `.setAttribute()`, and something called a “property” which is a field of the JavaScript class representation of that parsed HTML element.
>
> In the case of an `<input value=...>`, setting the `value` *attribute* is defined as setting the initial value for the input, and setting `value` *property* sets its current value. It maybe easiest to understand this by opening `about:blank` and running the following JavaScript in the browser console, line by line:
>
> ```js
> // create an input and append it to the DOM
> const el = document.createElement("input")
> document.body.appendChild(el)
>
> el.setAttribute("value", "test") // updates the input
> el.setAttribute("value", "another test") // updates the input again
>
> // now go and type into the input: delete some characters, etc.
>
> el.setAttribute("value", "one more time?")
> // nothing should have changed. setting the "initial value" does nothing now
>
> // however...
> el.value = "But this works"
> ```
>
> Many other frontend frameworks conflate attributes and properties, or create a special case for inputs that sets the value correctly. Maybe Leptos should do this too; but for now, I prefer giving users the maximum amount of control over whether theyre setting an attribute or a property, and doing my best to educate people about the actual underlying browser behavior rather than obscuring it.
## Uncontrolled Inputs
In an "uncontrolled input," the browser controls the state of the input element.

View File

@@ -208,7 +208,8 @@ view! { cx,
`<Show/>` memoizes the `when` condition, so it only renders its `<Small/>` once,
continuing to show the same component until `value` is greater than five;
then it renders `<Big/>` once, continuing to show it indefinitely.
then it renders `<Big/>` once, continuing to show it indefinitely or until `value`
goes below five and then renders `<Small/>` again.
This is a helpful tool to avoid rerendering when using dynamic `if` expressions.
As always, there's some overhead: for a very simple node (like updating a single

View File

@@ -115,11 +115,11 @@ Calling it like this will create a list:
```rust
view! { cx,
<WrappedChildren>
<WrapsChildren>
"A"
"B"
"C"
</WrappedChildren>
</WrapsChildren>
}
```

View File

@@ -5,32 +5,35 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
"counters",
"counters_stable",
"counter_without_macros",
"error_boundary",
"errors_axum",
"fetch",
"hackernews",
"hackernews_axum",
"js-framework-benchmark",
"leptos-tailwind-axum",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"slots",
"ssr_modes",
"ssr_modes_axum",
"tailwind",
"tailwind_csr_trunk",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
"animated_show",
"counter",
"counter_isomorphic",
"counters",
"counters_stable",
"counter_url_query",
"counter_without_macros",
"error_boundary",
"errors_axum",
"fetch",
"hackernews",
"hackernews_axum",
"js-framework-benchmark",
"leptos-tailwind-axum",
"login_with_token_csr_only",
"parent_child",
"router",
"session_auth_axum",
"slots",
"ssr_modes",
"ssr_modes_axum",
"suspense_tests",
"tailwind",
"tailwind_csr_trunk",
"timer",
"todo_app_sqlite",
"todo_app_sqlite_axum",
"todo_app_sqlite_viz",
"todomvc",
]
[tasks.gen-members]
@@ -45,3 +48,65 @@ grep -v gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
'''
[tasks.test-runner-report]
workspace = false
description = "report ci test runners for each example - OPTION: [all]"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
ITALIC="\e[3m"
YELLOW="\e[0;33m"
RESET="\e[0m"
echo
echo "${YELLOW}Test Runner Report${RESET}"
echo "${ITALIC}Pass the option \"all\" to show all the examples${RESET}"
echo
makefile_paths=$(find . -name Makefile.toml -not -path '*/target/*' |
sed 's%./%%' |
sed 's%/Makefile.toml%%' |
grep -v Makefile.toml |
sort -u)
start_path=$(pwd)
for path in $makefile_paths; do
cd $path
test_runner=
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
if [ $test_count -gt 0 ]; then
test_runner="-C"
fi
while read -r line; do
case $line in
*"wasm-test.toml"*)
test_runner=$test_runner"-W"
;;
*"playwright-test.toml"*)
test_runner=$test_runner"-P"
;;
*"cargo-leptos-test.toml"*)
test_runner=$test_runner"-L"
;;
esac
done <"./Makefile.toml"
if [ ! -z "$1" ]; then
# Show all examples
echo "$path ${BOLD}${test_runner}${RESET}"
elif [ ! -z $test_runner ]; then
# Filter out examples that do not run tests in `ci`
echo "$path ${BOLD}${test_runner}${RESET}"
fi
cd ${start_path}
done
echo
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
echo
'''

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
[tasks.install-cargo-leptos]
install_crate = { crate_name = "cargo-leptos", binary = "cargo-leptos", test_arg = "--help" }
[tasks.build]
clear = true
command = "cargo"
args = ["leptos", "build"]
[tasks.check]
clear = true
dependencies = ["check-debug", "check-release"]
[tasks.check-debug]
toolchain = "nightly"
command = "cargo"
args = ["check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-release]
toolchain = "nightly"
command = "cargo"
args = ["check-all-features", "--release"]
install_crate = "cargo-all-features"
[tasks.start-client]
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

@@ -0,0 +1,11 @@
[tasks.build]
toolchain = "nightly"
command = "cargo"
args = ["build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
toolchain = "nightly"
command = "cargo"
args = ["check-all-features"]
install_crate = "cargo-all-features"

View File

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

View File

@@ -0,0 +1,30 @@
[tasks.start-webdriver]
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
if command -v chromedriver; then
if [ -z $(pidof chromedriver) ]; then
chromedriver --port=4444 &
fi
else
echo "${RED}${BOLD}ERROR${RESET} - chromedriver is required by this task"
exit 1
fi
'''
[tasks.stop-webdriver]
script = '''
pkill -f "chromedriver"
'''
[tasks.webdriver-status]
script = '''
if [ -z $(pidof chromedriver) ]; then
echo chromedriver is not running
else
echo chromedriver is up
fi
'''

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

View File

@@ -4,11 +4,13 @@ extend = [
]
[tasks.build]
toolchain = "stable"
command = "cargo"
args = ["+stable", "build-all-features"]
args = ["build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
toolchain = "stable"
command = "cargo"
args = ["+stable", "check-all-features"]
args = ["check-all-features"]
install_crate = "cargo-all-features"

View File

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

View File

@@ -6,6 +6,13 @@ extend = [
]
[tasks.build]
toolchain = "stable"
command = "cargo"
args = ["+stable", "build-all-features"]
args = ["build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
toolchain = "stable"
command = "cargo"
args = ["check-all-features"]
install_crate = "cargo-all-features"

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ pub fn App(cx: Scope) -> impl IntoView {
</header>
<main>
<Routes>
<Route path="" view=|cx| view! { cx, <ExampleErrors/> }/>
<Route path="" view=ExampleErrors/>
</Routes>
</main>
</Router>

View File

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

View File

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

View File

@@ -17,6 +17,14 @@ 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
leptos::on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
let json = gloo_net::http::Request::get(path)
.abort_signal(abort_signal.as_ref())
.send()
@@ -27,13 +35,6 @@ where
.await
.ok()?;
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
leptos::on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
T::de(&json).ok()
}

View File

@@ -21,7 +21,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>

View File

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

View File

@@ -17,6 +17,14 @@ 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
leptos::on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
let json = gloo_net::http::Request::get(path)
.abort_signal(abort_signal.as_ref())
.send()
@@ -27,13 +35,6 @@ where
.await
.ok()?;
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
leptos::on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
T::de(&json).ok()
}

View File

@@ -21,7 +21,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>

View File

@@ -0,0 +1,15 @@
# fly.toml app configuration file generated for leptos-hackernews-islands on 2023-07-27T08:08:20-04:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "leptos-hackernews-islands"
primary_region = "bos"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

View File

@@ -4,13 +4,15 @@ extend = [
]
[tasks.build]
toolchain = "nightly"
command = "cargo"
args = ["+nightly", "build-all-features", "--target", "wasm32-unknown-unknown"]
args = ["build-all-features", "--target", "wasm32-unknown-unknown"]
install_crate = "cargo-all-features"
[tasks.check]
toolchain = "nightly"
command = "cargo"
args = ["+nightly", "check-all-features", "--target", "wasm32-unknown-unknown"]
args = ["check-all-features", "--target", "wasm32-unknown-unknown"]
install_crate = "cargo-all-features"
[tasks.pre-clippy]

View File

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

View File

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

View File

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

View File

@@ -3,13 +3,3 @@ extend = [
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

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

View File

@@ -170,7 +170,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
<hr/>
<main>
<Routes>
<Route path="" view=|cx| view! { cx, <Todos/> }/> //Route
<Route path="" view=Todos/> //Route
<Route path="signup" view=move |cx| view! {
cx,
<Signup action=signup/>

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
[package]
name = "leptos_start"
name = "suspense_tests"
version = "0.1.0"
edition = "2021"
@@ -12,9 +12,9 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "1"
cfg-if = "1"
leptos = { path = "../../..", features = ["serde"] }
leptos_actix = { path = "../../../../integrations/actix", optional = true }
leptos_router = { path = "../../../../router"}
leptos = { path = "../../leptos", features = ["serde"] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
log = "0.4"
simple_logger = "4"
wasm-bindgen = "0.2.87"
@@ -34,7 +34,7 @@ ssr = [
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos_start"
output-name = "suspense_tests"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
@@ -47,8 +47,8 @@ reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
@@ -74,5 +74,3 @@ lib-features = ["hydrate"]
#
# Optional. Defaults to false.
lib-default-features = false
[workspace]

View File

@@ -0,0 +1,24 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/webdriver.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
APP_PROCESS_NAME = "suspense_tests"
[tasks.integration-test]
dependencies = [
"install-cargo-leptos",
"start-webdriver",
"test-e2e-with-auto-start",
]
[tasks.test-e2e-with-auto-start]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.test-ui]
cwd = "./e2e"
command = "cargo"
args = ["make", "test-ui", "${@}"]

View File

@@ -47,15 +47,35 @@ After running a `cargo leptos build --release` the minimum files needed are:
Copy these files to your remote server. The directory structure should be:
```text
leptos_start
suspense_tests
site/
```
Set the following enviornment variables (updating for your project as needed):
```text
LEPTOS_OUTPUT_NAME="leptos_start"
LEPTOS_OUTPUT_NAME="suspense_tests"
LEPTOS_SITE_ROOT="site"
LEPTOS_SITE_PKG_DIR="pkg"
LEPTOS_SITE_ADDR="127.0.0.1:3000"
LEPTOS_RELOAD_PORT="3001"
```
Finally, run the server binary.
## Testing
This example includes quality checks and end-to-end testing.
To get started run this once.
```bash
cargo make ci
```
To only run the UI tests...
```bash
cargo make start-webdriver
cargo leptos watch # or cargo run...
cargo make test-ui
```
_See the [E2E README](./e2e/README.md) for more information about the testing strategy._

View File

@@ -0,0 +1,18 @@
[package]
name = "suspense_tests_e2e"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
anyhow = "1.0.72"
async-trait = "0.1.72"
cucumber = "0.19.1"
fantoccini = "0.19.3"
pretty_assertions = "1.4.0"
serde_json = "1.0.104"
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] }
url = "2.4.0"
[[test]]
name = "app_suite"
harness = false # Allow Cucumber to print output instead of libtest

View File

@@ -0,0 +1,11 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.test]
env = { RUN_AUTOMATICALLY = false }
condition = { env_true = ["RUN_AUTOMATICALLY"] }
[tasks.ci]
[tasks.test-ui]
command = "cargo"
args = ["test", "--test", "app_suite", "--", "--fail-fast", "${@}"]

View File

@@ -0,0 +1,34 @@
# E2E Testing
This example demonstrates e2e testing with Rust using executable requirements.
## Testing Stack
| | Role | Description |
|---|---|---|
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome
## Testing Organization
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
Here is a brief overview of how things fit together.
```bash
features
└── {action}_{object}.feature # Specify test scenarios
tests
├── fixtures
│ ├── action.rs # Perform a user action (click, type, etc.)
│ ├── check.rs # Assert what a user can see/not see
│ ├── find.rs # Query page elements
│ ├── mod.rs
│ └── world
│ ├── action_steps.rs # Map Gherkin steps to user actions
│ ├── check_steps.rs # Map Gherkin steps to user expectations
│ └── mod.rs
└── app_suite.rs # Test main
```

View File

@@ -0,0 +1,19 @@
@click_inside_component_count
Feature: Click Inside Component Count
Background:
Given I see the app
Scenario Outline: Should increase the count
Given I select the mode <Mode>
And I select the component Inside Component
When I click the count 3 times
Then I see the count is 3
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,19 @@
@click_nested_count
Feature: Click Nested Count
Background:
Given I see the app
Scenario Outline: Should increase the count
Given I select the mode <Mode>
And I select the component Nested
When I click the count 3 times
Then I see the count is 3
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,19 @@
@click_nested_inside_count
Feature: Click Nested Inside Count
Background:
Given I see the app
Scenario Outline: Should increase the count
Given I select the mode <Mode>
And I select the component Nested (resource created inside)
When I click the count 3 times
Then I see the count is 3
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,20 @@
@click_no_resources_counts
Feature: Click No Resources Count (1)
Background:
Given I see the app
Scenario Outline: Should increase the first and second counts
Given I select the mode <Mode>
And I select the component No Resources
When I click the first count 3 times
Then I see the first count is 3
And I see the second count is 3
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,20 @@
@click_no_resources_counts_2
Feature: Click No Resources Count (2)
Background:
Given I see the app
Scenario Outline: Should increase the first and second counts
Given I select the mode <Mode>
And I select the component No Resources
When I click the second count 3 times
Then I see the first count is 3
And I see the second count is 3
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,20 @@
@click_parallel_counts_1
Feature: Click Parallel Count (1)
Background:
Given I see the app
Scenario Outline: Should increase the first and second counts
Given I select the mode <Mode>
And I select the component Parallel
When I click the first count 3 times
Then I see the first count is 3
And I see the second count is 3
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,20 @@
@click_parallel_counts_2
Feature: Click Parallel Count (2)
Background:
Given I see the app
Scenario Outline: Should increase the first and second counts
Given I select the mode <Mode>
And I select the component Parallel
When I click the second count 3 times
Then I see the first count is 3
And I see the second count is 3
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,19 @@
@click_single_count
Feature: Click Single Count
Background:
Given I see the app
Scenario Outline: Should increase the count
Given I select the mode <Mode>
And I select the component Single
When I click the count 3 times
Then I see the count is 3
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,7 @@
@open_app
Feature: Open App
@open_app-title
Scenario: Should see the initial page title
When I open the app
Then I see the page title is Out-of-Order

View File

@@ -0,0 +1,55 @@
@view_inside_component
Feature: View Inside Component
Background:
Given I see the app
@view_inside_component
Scenario Outline: Should see the page title
Given I select the mode <Mode>
When I select the component Inside Component
Then I see the page title is <Mode>
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_inside_component-one
Scenario Outline: Should see the one second message
Given I select the mode <Mode>
When I select the component Inside Component
Then I see the one second message is One Second: Loaded 1!
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_inside_component-inside
Scenario Outline: Should see the inside message
Given I select the mode <Mode>
When I select the component Inside Component
Then I see the inside message is Suspense inside another component should work.
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_inside_component-following
Scenario Outline: Should see the following message
Given I select the mode <Mode>
When I select the component Inside Component
Then I see the following message is Children following Suspense should hydrate properly.
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,45 @@
@view_nested
Feature: View Nested
Background:
Given I see the app
@view_nested-title
Scenario Outline: Should see the page title
Given I select the mode <Mode>
When I select the component Nested
Then I see the page title is <Mode>
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_nested-one
Scenario Outline: Should see the one second message
Given I select the mode <Mode>
When I select the component Nested
Then I see the one second message is One Second: Loaded 1!
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_nested-two
Scenario Outline: Should see the two second message
Given I select the mode <Mode>
When I select the component Nested
Then I see the two second message is Two Second: Loaded 2!
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,42 @@
@view_nested_inside
Feature: View Nested Inside
Background:
Given I see the app
@view_nested_inside-title
Scenario Outline: Should see the page title
Given I select the mode <Mode>
When I select the component Nested (resource created inside)
Then I see the page title is <Mode>
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_nested_inside-one
Scenario Outline: Should see the one second message
Given I select the mode <Mode>
When I select the component Nested (resource created inside)
Then I see the one second message is One Second: Loaded 1!
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_nested_inside-two
Scenario Outline: Should see the two second message
Given I select the mode <Mode>
When I select the component Nested (resource created inside)
Then I see the two second message is Loaded 2 (created inside first suspense)!: Ok(())
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,42 @@
@view_no_resources
Feature: view No Resources
Background:
Given I see the app
@view_no_resources-title
Scenario Outline: Should see the page title
Given I select the mode <Mode>
When I select the component No Resources
Then I see the page title is <Mode>
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_no_resources-another
Scenario Outline: Should see the inside message
Given I select the mode <Mode>
When I select the component No Resources
Then I see the inside message is Children inside Suspense should hydrate properly.
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_no_resources-following
Scenario Outline: Should see the following message
Given I select the mode <Mode>
When I select the component No Resources
Then I see the following message is Children following Suspense should hydrate properly.
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,42 @@
@view_parallel
Feature: View Parallel
Background:
Given I see the app
@view_parallel-title
Scenario Outline: Should see the page title
Given I select the mode <Mode>
When I select the component Parallel
Then I see the page title is <Mode>
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_parallel-one
Scenario Outline: Should see the one second message
Given I select the mode <Mode>
When I select the component Parallel
Then I see the one second message is One Second: Loaded 1!
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_parallel-two
Scenario Outline: Should see the two second message
Given I select the mode <Mode>
When I select the component Parallel
Then I see the two second message is Two Second: Loaded 2!
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,42 @@
@view_single
Feature: View Single
Background:
Given I see the app
@view_single-title
Scenario Outline: Should see the page title
Given I select the mode <Mode>
When I select the component Single
Then I see the page title is <Mode>
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_single-one
Scenario Outline: Should see the one second message
Given I select the mode <Mode>
When I select the component Single
Then I see the one second message is One Second: Loaded 1!
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |
@view_single-following
Scenario Outline: Should see the following message
Given I select the mode <Mode>
When I select the component Single
Then I see the following message is Children following Suspense should hydrate properly.
Examples:
| Mode |
| Out-of-Order |
| In-Order |
| Async |

View File

@@ -0,0 +1,14 @@
mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
#[tokio::main]
async fn main() -> Result<()> {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit("./features")
.await;
Ok(())
}

View File

@@ -0,0 +1,39 @@
use super::{find, world::HOST};
use anyhow::Result;
use fantoccini::{Client, Locator};
use std::result::Result::Ok;
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
let url = format!("{}{}", HOST, path);
client.goto(&url).await?;
Ok(())
}
pub async fn click_link(client: &Client, text: &str) -> Result<()> {
let link = client
.wait()
.for_element(Locator::LinkText(text))
.await
.expect(format!("Link not found by `{}`", text).as_str());
link.click().await?;
Ok(())
}
pub async fn click_first_button(client: &Client) -> Result<()> {
let counter_button = find::first_button(client).await?;
counter_button.click().await?;
Ok(())
}
pub async fn click_second_button(client: &Client) -> Result<()> {
let counter_button = find::second_button(client).await?;
counter_button.click().await?;
Ok(())
}

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