Compare commits

..

16 Commits
1952 ... v0.5.4

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

* chore(ci): simulate leptos source change

* ci(todo_app_sqlite_csr): increase retries

* ci: delete check examples workflow

* ci: rename ci examples workflow

* ci: run ci examples with stable toolchain

* chore(ci): remove simulated change

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

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

* fix indentation
2023-11-18 08:24:30 -05:00
Greg Johnston
e47a619556 examples: add CSR with server functions example (closes #1975) (#2031) 2023-11-18 08:24:15 -05:00
88 changed files with 1444 additions and 216 deletions

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
name: CI Examples
on:
workflow_dispatch:
push:
tags:
- v*
schedule:
# Run once a day at 3:00 AM EST
- cron: "0 8 * * *"
jobs:
get-examples-matrix:
uses: ./.github/workflows/get-examples-matrix.yml
test:
name: CI
needs: [get-examples-matrix]
strategy:
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

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

View File

@@ -40,6 +40,8 @@ Example projects depend on the following tools. Please install them as needed.
- [Cargo Make](https://sagiegurari.github.io/cargo-make/)
- Run `cargo install --force cargo-make`
- Setup a command alias like `alias cm='cargo make'` to reduce typing (**_Optional_**)
- [Trunk](https://github.com/thedodd/trunk)
- Run `cargo install trunk`
- [Node Version Manager](https://github.com/nvm-sh/nvm/) (**_Optional_**)
- [Node.js](https://nodejs.org/)
- [pnpm](https://pnpm.io/) (**_Optional_**)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,3 +6,7 @@ This example creates a large table with randomized entries, it also shows usage
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -12,3 +12,7 @@ This example highlights four different ways that child components can communicat
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -12,4 +12,5 @@ console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
wasm-bindgen = "0.2"
web-sys = "0.3"
web-sys = "0.3"
gloo-timers = { version = "0.3", features = ["futures"] }

View File

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

View File

@@ -6,8 +6,12 @@ use leptos::*;
use portal::App;
use web_sys::HtmlButtonElement;
async fn next_tick() {
gloo_timers::future::TimeoutFuture::new(25).await;
}
#[wasm_bindgen_test]
fn portal() {
async fn portal() {
let document = leptos::document();
let body = document.body().unwrap();
@@ -24,7 +28,7 @@ fn portal() {
show_button.click();
// next_tick().await;
next_tick().await;
// check HTML
assert_eq!(

View File

@@ -5,3 +5,7 @@ This example demonstrates how Leptoss router works for client side routing.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -25,16 +25,16 @@ tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
sqlx = { version = "0.6.2", features = [
sqlx = { version = "0.7.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
axum_session_auth = { version = "0.2.1", features = [
axum_session_auth = { version = "0.9.0", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.2.3", features = [
axum_session = { version = "0.9.0", features = [
"sqlite-rustls",
], optional = true }
bcrypt = { version = "0.14", optional = true }

View File

@@ -5,3 +5,7 @@ This example creates a basic todo app with an Axum backend that uses Leptos' ser
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

View File

@@ -56,8 +56,7 @@ if #[cfg(feature = "ssr")] {
// Auth section
let session_config = SessionConfig::default().with_table_name("axum_sessions");
let auth_config = AuthConfig::<i64>::default();
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config);
session_store.initiate().await.unwrap();
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
sqlx::migrate!()
.run(&pool)

View File

@@ -5,3 +5,7 @@ This example shows how to use Slots in Leptos.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -9,18 +9,25 @@ See the [Examples README](../README.md) for setup and run instructions.
## Server-Side Rendering Modes
1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the client, replacing `fallback` once they're loaded.
- *Pros*: App shell appears very quickly: great TTFB (time to first byte).
- *Cons*: Resources load relatively slowly; you need to wait for JS + Wasm to load before even making a request.
- _Pros_: App shell appears very quickly: great TTFB (time to first byte).
- _Cons_: Resources load relatively slowly; you need to wait for JS + Wasm to load before even making a request.
2. **Out-of-order streaming**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
- *Pros*: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
- *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
- _Pros_: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
- _Cons_: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
3. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
- *Pros*: Does not require JS for HTML to appear in correct order.
- *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
of the page will not be interactive until the suspended chunks have loaded.
- _Pros_: Does not require JS for HTML to appear in correct order.
- _Cons_: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
of the page will not be interactive until the suspended chunks have loaded.
4. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
- *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
- *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
- _Pros_: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
- _Cons_: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -9,19 +9,25 @@ See the [Examples README](../README.md) for setup and run instructions.
## Server-Side Rendering Modes
1. **Synchronous**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the client, replacing `fallback` once they're loaded.
- *Pros*: App shell appears very quickly: great TTFB (time to first byte).
- *Cons*: Resources load relatively slowly; you need to wait for JS + Wasm to load before even making a request.
- _Pros_: App shell appears very quickly: great TTFB (time to first byte).
- _Cons_: Resources load relatively slowly; you need to wait for JS + Wasm to load before even making a request.
2. **Out-of-order streaming**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
- *Pros*: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
- *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
- _Pros_: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
- _Cons_: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
3. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
- *Pros*: Does not require JS for HTML to appear in correct order.
- *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
of the page will not be interactive until the suspended chunks have loaded.
- _Pros_: Does not require JS for HTML to appear in correct order.
- _Cons_: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
of the page will not be interactive until the suspended chunks have loaded.
4. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
- *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
- *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
- _Pros_: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
- _Cons_: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -9,3 +9,7 @@ See the [Examples README](../README.md) for setup and run instructions.
## Test Strategy
See the [E2E README](./e2e/README.md) to learn about the web testing strategy for this project.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -5,3 +5,7 @@ This is a template demonstrating how to integrate [TailwindCSS](https://tailwind
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -51,3 +51,7 @@ Allow vscode Ports forward: 3000, 3001.
### Attribution
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/leptos-rs/leptos/discussions/125).
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -5,3 +5,7 @@ This example creates a simple timer based on `setInterval` in a client side rend
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -13,3 +13,7 @@ See the [E2E README](./e2e/README.md) for more information about the testing str
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -13,3 +13,7 @@ See the [E2E README](./e2e/README.md) for more information about the testing str
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -0,0 +1,95 @@
[package]
name = "todo_app_sqlite_csr"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1", features = ["derive"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
wasm-bindgen = "0.2"
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:leptos_integration_utils",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "todo_app_sqlite_csr"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "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 tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["csr"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

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

View File

@@ -0,0 +1,12 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
CLIENT_PROCESS_NAME = "todo_app_sqlite_csr"
[tasks.test-ui]
cwd = "./e2e"
command = "cargo"
args = ["make", "test-ui", "${@}"]

View File

@@ -0,0 +1,19 @@
# Leptos Todo App Sqlite with CSR
This example shows how to combine client-side rendering with server functions, i.e., using server functions as a convenient way to create an ad hoc API, but without using server-side rendering and hydration.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

View File

@@ -0,0 +1,18 @@
[package]
name = "todo_app_sqlite_csr_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,20 @@
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",
"--",
"--retry",
"5",
"--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,16 @@
@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
@add_todo-style
Scenario: Should see the pending todo
When I add a todo as Buy Oranges
Then I see the pending todo

View File

@@ -0,0 +1,18 @@
@delete_todo
Feature: Delete Todo
Background:
Given I see the app
@serial
@delete_todo-remove
Scenario: Should not see the deleted todo
Given I add a todo as Buy Yogurt
When I delete the todo named Buy Yogurt
Then I do not see the todo named Buy Yogurt
@serial
@delete_todo-message
Scenario: Should see the empty list message
When I empty the todo list
Then I see the empty list message is No tasks were found.

View File

@@ -0,0 +1,12 @@
@open_app
Feature: Open App
@open_app-title
Scenario: Should see the home page title
When I open the app
Then I see the page title is My Tasks
@open_app-label
Scenario: Should see the input label
When I open the app
Then I see the label of the input is Add a Todo

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,60 @@
use super::{find, world::HOST};
use anyhow::Result;
use fantoccini::Client;
use std::result::Result::Ok;
use tokio::{self, time};
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
let url = format!("{}{}", HOST, path);
client.goto(&url).await?;
Ok(())
}
pub async fn add_todo(client: &Client, text: &str) -> Result<()> {
fill_todo(client, text).await?;
click_add_button(client).await?;
Ok(())
}
pub async fn fill_todo(client: &Client, text: &str) -> Result<()> {
let textbox = find::todo_input(client).await;
textbox.send_keys(text).await?;
Ok(())
}
pub async fn click_add_button(client: &Client) -> Result<()> {
let add_button = find::add_button(client).await;
add_button.click().await?;
Ok(())
}
pub async fn empty_todo_list(client: &Client) -> Result<()> {
let todos = find::todos(client).await;
for _todo in todos {
let _ = delete_first_todo(client).await?;
}
Ok(())
}
pub async fn delete_first_todo(client: &Client) -> Result<()> {
if let Some(element) = find::first_delete_button(client).await {
element.click().await.expect("Failed to delete todo");
time::sleep(time::Duration::from_millis(250)).await;
}
Ok(())
}
pub async fn delete_todo(client: &Client, text: &str) -> Result<()> {
if let Some(element) = find::delete_button(client, text).await {
element.click().await?;
time::sleep(time::Duration::from_millis(250)).await;
}
Ok(())
}

View File

@@ -0,0 +1,57 @@
use super::find;
use anyhow::{Ok, Result};
use fantoccini::{Client, Locator};
use pretty_assertions::assert_eq;
pub async fn text_on_element(
client: &Client,
selector: &str,
expected_text: &str,
) -> Result<()> {
let element = client
.wait()
.for_element(Locator::Css(selector))
.await
.expect(
format!("Element not found by Css selector `{}`", selector)
.as_str(),
);
let actual = element.text().await?;
assert_eq!(&actual, expected_text);
Ok(())
}
pub async fn todo_present(
client: &Client,
text: &str,
expected: bool,
) -> Result<()> {
let todo_present = is_todo_present(client, text).await;
assert_eq!(todo_present, expected);
Ok(())
}
async fn is_todo_present(client: &Client, text: &str) -> bool {
let todos = find::todos(client).await;
for todo in todos {
let todo_title = todo.text().await.expect("Todo title not found");
if todo_title == text {
return true;
}
}
false
}
pub async fn todo_is_pending(client: &Client) -> Result<()> {
if let None = find::pending_todo(client).await {
assert!(false, "Pending todo not found");
}
Ok(())
}

View File

@@ -0,0 +1,63 @@
use fantoccini::{elements::Element, Client, Locator};
pub async fn todo_input(client: &Client) -> Element {
let textbox = client
.wait()
.for_element(Locator::Css("input[name='title"))
.await
.expect("Todo textbox not found");
textbox
}
pub async fn add_button(client: &Client) -> Element {
let button = client
.wait()
.for_element(Locator::Css("input[value='Add']"))
.await
.expect("");
button
}
pub async fn first_delete_button(client: &Client) -> Option<Element> {
if let Ok(element) = client
.wait()
.for_element(Locator::Css("li:first-child input[value='X']"))
.await
{
return Some(element);
}
None
}
pub async fn delete_button(client: &Client, text: &str) -> Option<Element> {
let selector = format!("//*[text()='{text}']//input[@value='X']");
if let Ok(element) =
client.wait().for_element(Locator::XPath(&selector)).await
{
return Some(element);
}
None
}
pub async fn pending_todo(client: &Client) -> Option<Element> {
if let Ok(element) =
client.wait().for_element(Locator::Css(".pending")).await
{
return Some(element);
}
None
}
pub async fn todos(client: &Client) -> Vec<Element> {
let todos = client
.find_all(Locator::Css("li"))
.await
.expect("Todo List not found");
todos
}

View File

@@ -0,0 +1,4 @@
pub mod action;
pub mod check;
pub mod find;
pub mod world;

View File

@@ -0,0 +1,57 @@
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(())
}
#[given(regex = "^I set the todo as (.*)$")]
async fn i_set_the_todo_as(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::fill_todo(client, &text).await?;
Ok(())
}
#[when(regex = "I click the Add button$")]
async fn i_click_the_button(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::click_add_button(client).await?;
Ok(())
}
#[when(regex = "^I delete the todo named (.*)$")]
async fn i_delete_the_todo_named(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
action::delete_todo(client, text.as_str()).await?;
Ok(())
}
#[given("the todo list is empty")]
#[when("I empty the todo list")]
async fn i_empty_the_todo_list(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::empty_todo_list(client).await?;
Ok(())
}

View File

@@ -0,0 +1,67 @@
use crate::fixtures::{check, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::then;
#[then(regex = "^I see the page title is (.*)$")]
async fn i_see_the_page_title_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "h1", &text).await?;
Ok(())
}
#[then(regex = "^I see the label of the input is (.*)$")]
async fn i_see_the_label_of_the_input_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "label", &text).await?;
Ok(())
}
#[then(regex = "^I see the todo named (.*)$")]
async fn i_see_the_todo_is_present(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::todo_present(client, text.as_str(), true).await?;
Ok(())
}
#[then("I see the pending todo")]
async fn i_see_the_pending_todo(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::todo_is_pending(client).await?;
Ok(())
}
#[then(regex = "^I see the empty list message is (.*)$")]
async fn i_see_the_empty_list_message_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::text_on_element(client, "ul p", &text).await?;
Ok(())
}
#[then(regex = "^I do not see the todo named (.*)$")]
async fn i_do_not_see_the_todo_is_present(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::todo_present(client, text.as_str(), false).await?;
Ok(())
}

View File

@@ -0,0 +1,39 @@
pub mod action_steps;
pub mod check_steps;
use anyhow::Result;
use cucumber::World;
use fantoccini::{
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
};
pub const HOST: &str = "http://127.0.0.1:3000";
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct AppWorld {
pub client: Client,
}
impl AppWorld {
async fn new() -> Result<Self, anyhow::Error> {
let webdriver_client = build_client().await?;
Ok(Self {
client: webdriver_client,
})
}
}
async fn build_client() -> Result<Client, NewSessionError> {
let mut cap = Capabilities::new();
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
cap.insert("goog:chromeOptions".to_string(), arg);
let client = ClientBuilder::native()
.capabilities(cap)
.connect("http://localhost:4444")
.await?;
Ok(client)
}

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS todos
(
id INTEGER NOT NULL PRIMARY KEY,
title VARCHAR,
completed BOOLEAN
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,58 @@
use crate::errors::TodoAppError;
use leptos::{Errors, *};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<TodoAppError> = errors
.get()
.into_iter()
.filter_map(|(_, v)| v.downcast_ref::<TodoAppError>().cloned())
.collect();
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
#[cfg(feature = "ssr")]
{
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

View File

@@ -0,0 +1,21 @@
use http::status::StatusCode;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum TodoAppError {
#[error("Not Found")]
NotFound,
#[error("Internal Server Error")]
InternalServerError,
}
impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}

View File

@@ -0,0 +1,45 @@
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{Html, IntoResponse, Response as AxumResponse},
};
use leptos::LeptosOptions;
use leptos_integration_utils::html_parts_separated;
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_or_index_handler(
uri: Uri,
State(options): State<LeptosOptions>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let (head, tail) = html_parts_separated(&options, None);
Html(format!("{head}</head><body>{tail}")).into_response()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}

View File

@@ -0,0 +1,15 @@
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
pub mod todo;
#[cfg_attr(feature = "csr", wasm_bindgen::prelude::wasm_bindgen)]
pub fn hydrate() {
use crate::todo::*;
_ = console_log::init_with_level(log::Level::Error);
console_error_panic_hook::set_once();
leptos::mount_to_body(TodoApp);
}

View File

@@ -0,0 +1,52 @@
#[cfg(feature = "ssr")]
#[allow(unused)]
mod ssr_imports {
pub use axum::{
body::Body as AxumBody,
extract::{Path, State},
http::Request,
response::{Html, IntoResponse, Response},
routing::{get, post},
Router,
};
pub use leptos::*;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
pub use todo_app_sqlite_csr::{
fallback::file_or_index_handler, todo::*, *,
};
}
#[cfg(feature = "ssr")]
#[cfg_attr(feature = "ssr", tokio::main)]
async fn main() {
use ssr_imports::*;
simple_logger::init_with_level(log::Level::Error)
.expect("couldn't initialize logging");
let _conn = db().await.expect("couldn't connect to DB");
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.fallback(file_or_index_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
logging::log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// Only the server may directly connect to the database.
}

View File

@@ -0,0 +1,195 @@
use crate::error_template::ErrorTemplate;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
#[cfg(feature = "ssr")]
use sqlx::{Connection, SqliteConnection};
#[cfg(feature = "ssr")]
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
}
#[server(GetTodos, "/api")]
pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
let req_parts = use_context::<leptos_axum::RequestParts>();
if let Some(req_parts) = req_parts {
println!("Uri = {:?}", req_parts.uri);
}
use futures::TryStreamExt;
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows.try_next().await? {
todos.push(row);
}
// Add a random header(because why not)
// let mut res_headers = HeaderMap::new();
// res_headers.insert(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap());
// let res_parts = leptos_axum::ResponseParts {
// headers: res_headers,
// status: Some(StatusCode::IM_A_TEAPOT),
// };
// let res_options_outer = use_context::<leptos_axum::ResponseOptions>();
// if let Some(res_options) = res_options_outer {
// res_options.overwrite(res_parts).await;
// }
Ok(todos)
}
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
.execute(&mut conn)
.await
{
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
// The struct name and path prefix arguments are optional.
#[server]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())?)
}
#[component]
pub fn TodoApp() -> impl IntoView {
//let id = use_context::<String>();
provide_meta_context();
view! {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/todo_app_sqlite_csr.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" view=Todos/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn Todos() -> impl IntoView {
let add_todo = create_server_multi_action::<AddTodo>();
let delete_todo = create_server_action::<DeleteTodo>();
let submissions = add_todo.submissions();
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(
move || (add_todo.version().get(), delete_todo.version().get()),
move |_| get_todos(),
);
view! {
<div>
<MultiActionForm action=add_todo>
<label>
"Add a Todo"
<input type="text" name="title"/>
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
<Transition fallback=move || view! {<p>"Loading..."</p> }>
<ErrorBoundary fallback=|errors| view!{<ErrorTemplate errors=errors/>}>
{move || {
let existing_todos = {
move || {
todos.get()
.map(move |todos| match todos {
Err(e) => {
view! { <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view()
}
Ok(todos) => {
if todos.is_empty() {
view! { <p>"No tasks were found."</p> }.into_view()
} else {
todos
.into_iter()
.map(move |todo| {
view! {
<li>
{todo.title}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
})
.collect_view()
}
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view()
};
view! {
<ul>
{existing_todos}
{pending_todos}
</ul>
}
}
}
</ErrorBoundary>
</Transition>
</div>
}
}

View File

@@ -0,0 +1,3 @@
.pending {
color: purple;
}

View File

@@ -9,3 +9,7 @@ See the [Examples README](../README.md) for setup and run instructions.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -5,3 +5,7 @@ This is a Leptos implementation of the TodoMVC example common to many frameworks
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View File

@@ -7,7 +7,7 @@
//! To run in this environment, you need to disable the default feature set and enable
//! the `wasm` feature on `leptos_axum` in your `Cargo.toml`.
//! ```toml
//! leptos_axum = { version = "0.5.2", default-features = false, features = ["wasm"] }
//! leptos_axum = { version = "0.5.4", default-features = false, features = ["wasm"] }
//! ```
//!
//! ## Features

View File

@@ -192,9 +192,11 @@ mod error_boundary;
pub use error_boundary::*;
mod animated_show;
mod for_loop;
mod provider;
mod show;
pub use animated_show::*;
pub use for_loop::*;
pub use provider::*;
#[cfg(feature = "experimental-islands")]
pub use serde;
#[cfg(feature = "experimental-islands")]

40
leptos/src/provider.rs Normal file
View File

@@ -0,0 +1,40 @@
use leptos::*;
#[component]
/// Uses the context API to [`provide_context`] to its children and descendants,
/// without overwriting any contexts of the same type in its own reactive scope.
///
/// This prevents issues related to “context shadowing.”
///
/// ```rust
/// # use leptos::*;
/// #[component]
/// pub fn App() -> impl IntoView {
/// // each Provider will only provide the value to its children
/// view! {
/// <Provider value=1u8>
/// // correctly gets 1 from context
/// {use_context::<u8>().unwrap_or(0)}
/// </Provider>
/// <Provider value=2u8>
/// // correctly gets 2 from context
/// {use_context::<u8>().unwrap_or(0)}
/// </Provider>
/// // does not find any u8 in context
/// {use_context::<u8>().unwrap_or(0)}
/// }
/// }
/// ```
pub fn Provider<T>(
/// The value to be provided via context.
value: T,
children: Children,
) -> impl IntoView
where
T: Clone + 'static,
{
run_as_child(move || {
provide_context(value);
children()
})
}

View File

@@ -172,7 +172,8 @@ ssr = ["leptos_reactive/ssr"]
nightly = ["leptos_reactive/nightly"]
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
experimental-islands = ["leptos_reactive/experimental-islands"]
trace-component-props = []
[package.metadata.cargo-all-features]
denylist = ["nightly"]
denylist = ["nightly", "trace-component-props"]
skip_feature_sets = [["web", "ssr"]]

View File

@@ -386,11 +386,14 @@ attr_signal_type_optional!(MaybeProp<T>);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
#[doc(hidden)]
#[inline(never)]
#[track_caller]
pub fn attribute_helper(
el: &web_sys::Element,
name: Oco<'static, str>,
value: Attribute,
) {
#[cfg(debug_assertions)]
let called_at = std::panic::Location::caller();
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(f) => {
@@ -398,12 +401,26 @@ pub fn attribute_helper(
create_render_effect(move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone(), true);
attribute_expression(
&el,
&name,
new.clone(),
true,
#[cfg(debug_assertions)]
called_at,
);
}
new
});
}
_ => attribute_expression(el, &name, value, false),
_ => attribute_expression(
el,
&name,
value,
false,
#[cfg(debug_assertions)]
called_at,
),
};
}
@@ -414,6 +431,7 @@ pub(crate) fn attribute_expression(
attr_name: &str,
value: Attribute,
force: bool,
#[cfg(debug_assertions)] called_at: &'static std::panic::Location<'static>,
) {
use crate::HydrationCtx;
@@ -452,10 +470,25 @@ pub(crate) fn attribute_expression(
}
Attribute::Fn(f) => {
let mut v = f();
crate::debug_warn!(
"At {called_at}, you are providing a dynamic attribute \
with a nested function. For example, you might have a \
closure that returns another function instead of a \
value. This creates some added overhead. If possible, \
you should instead provide a function that returns a \
value instead.",
);
while let Attribute::Fn(f) = v {
v = f();
}
attribute_expression(el, attr_name, v, force);
attribute_expression(
el,
attr_name,
v,
force,
#[cfg(debug_assertions)]
called_at,
);
}
}
}

View File

@@ -2,6 +2,7 @@ mod into_attribute;
mod into_class;
mod into_property;
mod into_style;
#[cfg(feature = "trace-component-props")]
#[doc(hidden)]
pub mod tracing_property;
pub use into_attribute::*;

View File

@@ -1,3 +1,5 @@
use wasm_bindgen::UnwrapThrowExt;
#[macro_export]
/// Use for tracing property
macro_rules! tracing_props {
@@ -9,9 +11,8 @@ macro_rules! tracing_props {
);
};
($($prop:tt),+ $(,)?) => {
#[cfg(any(debug_assertions, feature = "ssr"))]
{
use ::leptos::leptos_dom::tracing_property::{Match, DebugMatch, DefaultMatch};
use ::leptos::leptos_dom::tracing_property::{Match, SerializeMatch, DefaultMatch};
let mut props = String::from('[');
$(
let prop = (&&Match {
@@ -39,17 +40,29 @@ pub struct Match<T> {
pub value: std::cell::Cell<Option<T>>,
}
pub trait DebugMatch {
pub trait SerializeMatch {
type Return;
fn spez(&self) -> Self::Return;
}
impl<T: core::fmt::Debug> DebugMatch for &Match<&T> {
impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
let debug_value =
format!("{:?}", self.value.get().unwrap()).replace('"', r#"\""#);
format!(r#"{{"name": "{name}", "value": "{debug_value}"}}"#,)
// suppresses warnings when serializing signals into props
#[cfg(debug_assertions)]
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
let value = serde_json::to_string(self.value.get().unwrap_throw())
.map_or_else(
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
);
#[cfg(debug_assertions)]
leptos_reactive::SpecialNonReactiveZone::exit(prev);
value
}
}
@@ -61,9 +74,7 @@ impl<T> DefaultMatch for Match<&T> {
type Return = String;
fn spez(&self) -> Self::Return {
let name = self.name;
format!(
r#"{{"name": "{name}", "value": "[value does not implement Debug]"}}"#
)
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
}
}
@@ -76,7 +87,7 @@ fn match_primitive() {
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "\"string\""}"#);
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// &str
let test = "string";
@@ -85,7 +96,7 @@ fn match_primitive() {
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(prop, r#"{"name": "test", "value": "\"string\""}"#);
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
// u128
let test: u128 = 1;
@@ -127,7 +138,7 @@ fn match_primitive() {
#[test]
fn match_serialize() {
use serde::Serialize;
#[derive(Debug)]
#[derive(Serialize)]
struct CustomStruct {
field: &'static str,
}
@@ -138,10 +149,7 @@ fn match_serialize() {
value: std::cell::Cell::new(Some(&test)),
})
.spez();
assert_eq!(
prop,
r#"{"name": "test", "value": "CustomStruct { field: \"field\" }"}"#
);
assert_eq!(prop, r#"{"name": "test", "value": {"field":"field"}}"#);
// Verification of ownership
assert_eq!(test.field, "field");
}
@@ -162,7 +170,7 @@ fn match_no_serialize() {
.spez();
assert_eq!(
prop,
r#"{"name": "test", "value": "[value does not implement Debug]"}"#
r#"{"name": "test", "value": "[unserializable value]"}"#
);
// Verification of ownership
assert_eq!(test.field, "field");

View File

@@ -43,7 +43,8 @@ ssr = ["server_fn_macro/ssr"]
nightly = ["server_fn_macro/nightly"]
tracing = []
experimental-islands = []
trace-component-props = []
[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing"]
denylist = ["nightly", "tracing", "trace-component-props"]
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]

View File

@@ -118,8 +118,6 @@ impl ToTokens for Model {
let no_props = props.is_empty();
let mut body = body.to_owned();
// check for components that end ;
if !is_transparent {
let ends_semi =
@@ -139,7 +137,6 @@ impl ToTokens for Model {
}
}
body.sig.ident = format_ident!("__{}", body.sig.ident);
#[allow(clippy::redundant_clone)] // false positive
let body_name = body.sig.ident.clone();
@@ -203,7 +200,7 @@ impl ToTokens for Model {
#[cfg(debug_assertions)]
let _guard = span.entered();
},
if no_props {
if no_props || !cfg!(feature = "trace-component-props") {
quote! {}
} else {
quote! {
@@ -234,6 +231,7 @@ impl ToTokens for Model {
quote! {}
};
let body_name = unmodified_fn_name_from_fn_name(&body_name);
let body_expr = if *is_island {
quote! {
::leptos::SharedContext::with_hydration(move || {
@@ -367,7 +365,6 @@ impl ToTokens for Model {
.collect::<TokenStream>();
let body = quote! {
#body
#destructure_props
#tracing_span_expr
#component
@@ -547,10 +544,10 @@ impl Model {
/// used to improve IDEs and rust-analyzer's auto-completion behavior in case
/// of a syntax error.
pub struct DummyModel {
attrs: Vec<Attribute>,
vis: Visibility,
sig: Signature,
body: TokenStream,
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub sig: Signature,
pub body: TokenStream,
}
impl Parse for DummyModel {
@@ -588,7 +585,21 @@ impl ToTokens for DummyModel {
let mut sig = sig.clone();
sig.inputs.iter_mut().for_each(|arg| {
if let FnArg::Typed(ty) = arg {
ty.attrs.clear();
ty.attrs.retain(|attr| match &attr.meta {
Meta::List(list) => list
.path
.segments
.first()
.map(|n| n.ident != "prop")
.unwrap_or(true),
Meta::NameValue(name_value) => name_value
.path
.segments
.first()
.map(|n| n.ident != "doc")
.unwrap_or(true),
_ => true,
});
}
});
sig
@@ -1162,3 +1173,7 @@ fn is_valid_into_view_return_type(ty: &ReturnType) -> bool {
.iter()
.any(|test| ty == test)
}
pub fn unmodified_fn_name_from_fn_name(ident: &Ident) -> Ident {
Ident::new(&format!("__{ident}"), ident.span())
}

View File

@@ -4,11 +4,12 @@
#[macro_use]
extern crate proc_macro_error;
use component::DummyModel;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree};
use quote::ToTokens;
use rstml::{node::KeyedAttribute, parse};
use syn::parse_macro_input;
use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Mode {
@@ -31,6 +32,7 @@ impl Default for Mode {
mod params;
mod view;
use crate::component::unmodified_fn_name_from_fn_name;
use view::{client_template::render_template, render_view};
mod component;
mod server;
@@ -598,21 +600,30 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
false
};
let parse_result = syn::parse::<component::Model>(s.clone());
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(s);
if let Ok(model) = parse_result {
model
.is_transparent(is_transparent)
.into_token_stream()
.into()
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.is_transparent(is_transparent).into_token_stream();
unexpanded.sig.ident =
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
quote! {
#expanded
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#unexpanded
}
} else if let Ok(mut dummy) = dummy {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#dummy
}
} else {
// When the input syntax is invalid, e.g. while typing, we let
// the dummy model output tokens similar to the input, which improves
// IDEs and rust-analyzer's auto-complete capabilities.
parse_macro_input!(s as component::DummyModel)
.into_token_stream()
.into()
quote! {}
}
.into()
}
/// Defines a component as an interactive island when you are using the
@@ -688,28 +699,36 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute]
pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(s);
if transparent != "transparent" {
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[island(transparent)]` or `#[island]`"
);
if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.is_island().into_token_stream();
if !matches!(unexpanded.vis, Visibility::Public(_)) {
unexpanded.vis = Visibility::Public(Pub {
span: unexpanded.vis.span(),
})
}
unexpanded.sig.ident =
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
quote! {
#expanded
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#unexpanded
}
} else if let Ok(mut dummy) = dummy {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#dummy
}
true
} else {
false
};
parse_macro_input!(s as component::Model)
.is_transparent(is_transparent)
.is_island()
.into_token_stream()
.into()
quote! {}
}
.into()
}
/// Annotates a struct so that it can be used with your Component as a `slot`.

View File

@@ -13,7 +13,6 @@ fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
#[component]
fn optional_and_optional_no_strip(
,
#[prop(optional, optional_no_strip)] conflicting: bool,
) -> impl IntoView {
_ = conflicting;
@@ -21,7 +20,6 @@ fn optional_and_optional_no_strip(
#[component]
fn optional_and_strip_option(
,
#[prop(optional, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = conflicting;
@@ -29,23 +27,18 @@ fn optional_and_strip_option(
#[component]
fn optional_no_strip_and_strip_option(
,
#[prop(optional_no_strip, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = conflicting;
}
#[component]
fn default_without_value(
,
#[prop(default)] default: bool,
) -> impl IntoView {
fn default_without_value(#[prop(default)] default: bool) -> impl IntoView {
_ = default;
}
#[component]
fn default_with_invalid_value(
,
#[prop(default= |)] default: bool,
) -> impl IntoView {
_ = default;

View File

@@ -1,33 +1,3 @@
error: expected parameter name, found `,`
--> tests/ui/component.rs:16:5
|
16 | ,
| ^ expected parameter name
error: expected parameter name, found `,`
--> tests/ui/component.rs:24:5
|
24 | ,
| ^ expected parameter name
error: expected parameter name, found `,`
--> tests/ui/component.rs:32:5
|
32 | ,
| ^ expected parameter name
error: expected parameter name, found `,`
--> tests/ui/component.rs:40:5
|
40 | ,
| ^ expected parameter name
error: expected parameter name, found `,`
--> tests/ui/component.rs:48:5
|
48 | ,
| ^ expected parameter name
error: return type is incorrect
--> tests/ui/component.rs:4:1
|
@@ -50,32 +20,34 @@ error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `de
10 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView {
| ^^^^^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:16:5
error: `optional` conflicts with mutually exclusive `optional_no_strip`
--> tests/ui/component.rs:16:12
|
16 | ,
| ^
16 | #[prop(optional, optional_no_strip)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:24:5
error: `optional` conflicts with mutually exclusive `strip_option`
--> tests/ui/component.rs:23:12
|
24 | ,
| ^
23 | #[prop(optional, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:32:5
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
--> tests/ui/component.rs:30:12
|
32 | ,
| ^
30 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:40:5
error: unexpected end of input, expected assignment `=`
--> tests/ui/component.rs:36:40
|
40 | ,
| ^
36 | fn default_without_value(#[prop(default)] default: bool) -> impl IntoView {
| ^
error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
--> tests/ui/component.rs:48:5
error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
= help: try `#[prop(default=5 * 10)]`
--> tests/ui/component.rs:42:22
|
48 | ,
| ^
42 | #[prop(default= |)] default: bool,
| ^

View File

@@ -84,7 +84,29 @@ use std::any::{Any, TypeId};
/// that was provided in `<Parent/>`, meaning that the second `<Child/>` receives the context
/// from its sibling instead.
///
/// This can be solved by introducing some additional reactivity. In this case, its simplest
/// ### Solution
///
/// If you are using the full Leptos framework, you can use the [`Provider`](leptos::Provider)
/// component to solve this issue.
///
/// ```rust
/// # use leptos::*;
/// #[component]
/// fn Child() -> impl IntoView {
/// let context = expect_context::<&'static str>();
/// // creates a new reactive node, which means the context will
/// // only be provided to its children, not modified in the parent
/// view! {
/// <Provider value="child_context">
/// <div>{format!("child (context: {context})")}</div>
/// </Provider>
/// }
/// }
/// ```
///
/// ### Alternate Solution
///
/// This can also be solved by introducing some additional reactivity. In this case, its simplest
/// to simply make the body of `<Child/>` a function, which means it will be wrapped in a
/// new reactive node when rendered:
/// ```rust

View File

@@ -1247,9 +1247,7 @@ where
}
#[cfg(all(feature = "hydrate", debug_assertions))]
{
if self.serializable != ResourceSerialization::Local
&& !SpecialNonReactiveZone::is_inside()
{
if self.serializable != ResourceSerialization::Local {
crate::macros::debug_warn!(
"At {location}, you are reading a resource in \
`hydrate` mode outside a <Suspense/> or \

View File

@@ -94,6 +94,7 @@ impl Trigger {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| {
runtime.update_if_necessary(self.id);
self.id.subscribe(runtime, diagnostics);
})
.is_ok()

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.5.2"
version = "0.5.4"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.5.2"
version = "0.5.4"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -217,7 +217,7 @@ where
target=target
prop:state=state.map(|s| s.to_js_value())
prop:replace=replace
aria-current=move || if is_active.get() { Some("a") } else { None }
aria-current=move || if is_active.get() { Some("page") } else { None }
class=class
id=id
>

View File

@@ -52,6 +52,7 @@ pub fn Outlet() -> impl IntoView {
prev_disposer.flatten()
}
(Some(child), _) => {
drop(prev_disposer);
is_showing.set(Some(child.id()));
let (outlet, disposer) = build_outlet(child);
set_outlet.set(Some(outlet));

View File

@@ -484,11 +484,11 @@ fn root_route(
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| {
let (outlet, disposer) = outlet((*route).clone());
drop(std::mem::replace(
drop(std::mem::take(
&mut *root_disposer.borrow_mut(),
Some(disposer),
));
let (outlet, disposer) = outlet((*route).clone());
*root_disposer.borrow_mut() = Some(disposer);
outlet
})
} else {