mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 12:31:55 -05:00
Compare commits
28 Commits
error
...
diagnostic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ad8f5ae4 | ||
|
|
6b3e9cf85f | ||
|
|
66f54e7f1a | ||
|
|
81e416b085 | ||
|
|
a5f73b441c | ||
|
|
0f1ebccad5 | ||
|
|
2f01df6185 | ||
|
|
c4982319fe | ||
|
|
8fb4e88439 | ||
|
|
e821efca07 | ||
|
|
568f7b21ae | ||
|
|
d3c0f5320c | ||
|
|
5adc88bf50 | ||
|
|
67300adf41 | ||
|
|
4a3a67bf37 | ||
|
|
8150847218 | ||
|
|
8cb95b4646 | ||
|
|
df4ce904a0 | ||
|
|
1cc3a43268 | ||
|
|
d5a862a406 | ||
|
|
33c83c3e62 | ||
|
|
171adcd09e | ||
|
|
13f7cb9a9a | ||
|
|
ee7dbafc85 | ||
|
|
f5cfe4e8a2 | ||
|
|
c3e45d19d7 | ||
|
|
966100c2d6 | ||
|
|
bce1dea11b |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -16,10 +16,10 @@ Please copy and paste the Leptos dependencies and features from your `Cargo.toml
|
||||
|
||||
For example:
|
||||
```toml
|
||||
leptos = { version = "0.3", default-features = false, features = ["serde"] }
|
||||
leptos = { version = "0.3", features = ["serde"] }
|
||||
leptos_axum = { version = "0.3", optional = true }
|
||||
leptos_meta = { version = "0.3", default-features = false }
|
||||
leptos_router = { version = "0.3", default-features = false }
|
||||
leptos_meta = { version = "0.3"}
|
||||
leptos_router = { version = "0.3"}
|
||||
```
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
30
Cargo.toml
30
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
resolver="2"
|
||||
resolver = "2"
|
||||
members = [
|
||||
# core
|
||||
"leptos",
|
||||
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0" }
|
||||
leptos_router = { path = "./router", version = "0.3.0" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
|
||||
leptos = { path = "./leptos", version = "0.4.0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.0" }
|
||||
leptos_router = { path = "./router", version = "0.4.0" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
10
README.md
10
README.md
@@ -68,7 +68,7 @@ Here are some resources for learning more about Leptos:
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` version of Rust. For this, you can either set your toolchain globally or on per-project basis.
|
||||
Most of the examples assume you’re using `nightly` version of Rust and the `nightly` feature of Leptos. To use `nightly` Rust, you can either set your toolchain globally or on per-project basis.
|
||||
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
@@ -86,13 +86,9 @@ channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
```
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
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.
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
for examples of the correct API.
|
||||
> 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`
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
leptos = { path = "../leptos", features = ["ssr"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
tokio-test = "0.4"
|
||||
|
||||
@@ -31,7 +31,7 @@ cargo init leptos-tutorial
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
cargo add leptos
|
||||
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
|
||||
```
|
||||
|
||||
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
|
||||
@@ -62,6 +62,8 @@ 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).
|
||||
|
||||
## A Note about Data-Loading Patterns
|
||||
|
||||
Because Actix and (especially) Axum are built on the idea of a single round-trip HTTP request and response, you typically run extractors near the “top” of your application (i.e., before you start rendering) and use the extracted data to determine how that should be rendered. Before you render a `<button>`, you load all the data your app could need. And any given route handler needs to know all the data that will need to be extracted by that route.
|
||||
|
||||
@@ -1 +1,74 @@
|
||||
# Responses and Redirects
|
||||
|
||||
Extractors provide an easy way to access request data inside server functions. Leptos also provides a way to modify the HTTP response, using the `ResponseOptions` type (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/struct.ResponseOptions.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/struct.ResponseOptions.html)) types and the `redirect` helper function (see docs for [Actix](https://docs.rs/leptos_actix/latest/leptos_actix/fn.redirect.html) or [Axum](https://docs.rs/leptos_axum/latest/leptos_axum/fn.redirect.html)).
|
||||
|
||||
## `ResponseOptions`
|
||||
|
||||
`ResponseOptions` is provided via context during the initial server rendering response and during any subsequent server function call. It allows you to easily set the status code for the HTTP response, or to add headers to the HTTP response, e.g., to set cookies.
|
||||
|
||||
```rust
|
||||
#[server(TeaAndCookies)]
|
||||
pub async fn tea_and_cookies(cx: Scope) -> Result<(), ServerFnError> {
|
||||
use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue};
|
||||
use leptos_actix::ResponseOptions;
|
||||
|
||||
// pull ResponseOptions from context
|
||||
let response = expect_context::<ResponseOptions>(cx);
|
||||
|
||||
// set the HTTP status code
|
||||
response.set_status(StatusCode::IM_A_TEAPOT);
|
||||
|
||||
// set a cookie in the HTTP response
|
||||
let mut cookie = Cookie::build("biscuits", "yes").finish();
|
||||
if let Ok(cookie) = HeaderValue::from_str(&cookie.to_string()) {
|
||||
res.insert_header(header::SET_COOKIE, cookie);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `redirect`
|
||||
|
||||
One common modification to an HTTP response is to redirect to another page. The Actix and Axum integrations provide a `redirect` function to make this easy to do. `redirect` simply sets an HTTP status code of `302 Found` and sets the `Location` header.
|
||||
|
||||
Here’s a simplified example from our [`session_auth_axum` example](https://github.com/leptos-rs/leptos/blob/a5f73b441c079f9138102b3a7d8d4828f045448c/examples/session_auth_axum/src/auth.rs#L154-L181).
|
||||
|
||||
```rust
|
||||
#[server(Login, "/api")]
|
||||
pub async fn login(
|
||||
cx: Scope,
|
||||
username: String,
|
||||
password: String,
|
||||
remember: Option<String>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
// pull the DB pool and auth provider from context
|
||||
let pool = pool(cx)?;
|
||||
let auth = auth(cx)?;
|
||||
|
||||
// check whether the user exists
|
||||
let user: User = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
ServerFnError::ServerError("User does not exist.".into())
|
||||
})?;
|
||||
|
||||
// check whether the user has provided the correct password
|
||||
match verify(password, &user.password)? {
|
||||
// if the password is correct...
|
||||
true => {
|
||||
// log the user in
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
|
||||
// and redirect to the home page
|
||||
leptos_axum::redirect(cx, "/");
|
||||
Ok(())
|
||||
}
|
||||
// if not, return an error
|
||||
false => Err(ServerFnError::ServerError(
|
||||
"Password does not match.".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This server function can then be used from your application. This `redirect` works well with the progressively-enhanced `<ActionForm/>` component: without JS/WASM, the server response will redirect because of the status code and header. With JS/WASM, the `<ActionForm/>` will detect the redirect in the server function response, and use client-side navigation to redirect to the new page.
|
||||
|
||||
@@ -34,7 +34,7 @@ category = "Cleanup"
|
||||
script = '''
|
||||
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
|
||||
do
|
||||
rm -rf $pw_dir/playwright-report
|
||||
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
|
||||
done
|
||||
'''
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.ci]
|
||||
alias = "verify-flow"
|
||||
|
||||
[tasks.verify-flow]
|
||||
description = "Provides pre and post hooks for verify"
|
||||
dependencies = ["pre-verify", "verify", "post-verify"]
|
||||
|
||||
7
examples/cargo-make/playwright-test.toml
Normal file
7
examples/cargo-make/playwright-test.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
extend = [{ path = "../cargo-make/playwright.toml" }]
|
||||
|
||||
[tasks.test-e2e]
|
||||
dependencies = ["setup-node", "test-playwright-autostart"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
119
examples/cargo-make/playwright.toml
Normal file
119
examples/cargo-make/playwright.toml
Normal file
@@ -0,0 +1,119 @@
|
||||
[tasks.clean-playwright]
|
||||
description = "Delete playwright directories"
|
||||
category = "Cleanup"
|
||||
script = '''
|
||||
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
|
||||
do
|
||||
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-autostart]
|
||||
description = "Run playwright test with server autostart"
|
||||
category = "Test"
|
||||
command = "npm"
|
||||
args = ["run", "e2e:auto-start"]
|
||||
|
||||
[tasks.test-playwright]
|
||||
description = "Run playwright test"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright test
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-ui]
|
||||
description = "Run playwright test --ui"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright test --ui
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-report]
|
||||
description = "Run playwright show-report"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright show-report
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.pw]
|
||||
dependencies = ["test-playwright"]
|
||||
|
||||
[tasks.pw-ui]
|
||||
dependencies = ["test-playwright-ui"]
|
||||
|
||||
[tasks.pw-report]
|
||||
dependencies = ["test-playwright-report"]
|
||||
22
examples/cargo-make/trunk_server.toml
Normal file
22
examples/cargo-make/trunk_server.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[tasks.build]
|
||||
command = "trunk"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.start-trunk]
|
||||
command = "trunk"
|
||||
args = ["serve", "--open"]
|
||||
|
||||
[tasks.stop-trunk]
|
||||
script = '''
|
||||
pkill -f "cargo-make"
|
||||
pkill -f "trunk"
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.dev]
|
||||
dependencies = ["start-trunk"]
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -19,19 +19,17 @@ console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "=0.2.86"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["nightly"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
@@ -41,10 +39,10 @@ ssr = [
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
stable = ["leptos/stable", "leptos_router/stable"]
|
||||
nightly = ["leptos/nightly", "leptos_router/nightly"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "stable"]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "nightly"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
20
examples/counters_stable/.gitignore
vendored
Normal file
20
examples/counters_stable/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Support trunk
|
||||
dist
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
|
||||
4
examples/counters_stable/e2e/.gitignore
vendored
Normal file
4
examples/counters_stable/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
83
examples/counters_stable/e2e/package-lock.json
generated
Normal file
83
examples/counters_stable/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "grip",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "grip",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@playwright+test@1.33.0": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
|
||||
"version": "20.2.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
examples/counters_stable/e2e/package.json
Normal file
7
examples/counters_stable/e2e/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
}
|
||||
77
examples/counters_stable/e2e/playwright.config.ts
Normal file
77
examples/counters_stable/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !process.env.DEV,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.DEV ? 0 : 10,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.DEV ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:8080",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: "cd ../ && trunk serve",
|
||||
// url: "http://127.0.0.1:8080",
|
||||
// reuseExistingServer: false, //!process.env.CI,
|
||||
// },
|
||||
});
|
||||
20
examples/counters_stable/e2e/tests/add_1k_counters.spec.ts
Normal file
20
examples/counters_stable/e2e/tests/add_1k_counters.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add 1000 Counters", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
|
||||
await Promise.all([
|
||||
await ui.goto(),
|
||||
await ui.addOneThousandCountersButton.waitFor(),
|
||||
]);
|
||||
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("3000");
|
||||
});
|
||||
});
|
||||
16
examples/counters_stable/e2e/tests/add_counter.spec.ts
Normal file
16
examples/counters_stable/e2e/tests/add_counter.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add Counter", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
18
examples/counters_stable/e2e/tests/clear_counters.spec.ts
Normal file
18
examples/counters_stable/e2e/tests/clear_counters.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Clear Counters", () => {
|
||||
test("should reset the counts", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.clearCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
17
examples/counters_stable/e2e/tests/decrement_count.spec.ts
Normal file
17
examples/counters_stable/e2e/tests/decrement_count.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Decrement Count", () => {
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("-3");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
});
|
||||
31
examples/counters_stable/e2e/tests/enter_count.spec.ts
Normal file
31
examples/counters_stable/e2e/tests/enter_count.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Enter Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.enterCount("5");
|
||||
|
||||
await expect(ui.total).toHaveText("5");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.enterCount("100");
|
||||
await ui.enterCount("100", 1);
|
||||
await ui.enterCount("100", 2);
|
||||
await ui.enterCount("50", 1);
|
||||
|
||||
await expect(ui.total).toHaveText("250");
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
98
examples/counters_stable/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
98
examples/counters_stable/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
export class CountersPage {
|
||||
readonly page: Page;
|
||||
readonly addCounterButton: Locator;
|
||||
readonly addOneThousandCountersButton: Locator;
|
||||
readonly clearCountersButton: Locator;
|
||||
|
||||
readonly incrementCountButton: Locator;
|
||||
readonly counterInput: Locator;
|
||||
readonly decrementCountButton: Locator;
|
||||
readonly removeCountButton: Locator;
|
||||
|
||||
readonly total: Locator;
|
||||
readonly counters: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
|
||||
|
||||
this.addOneThousandCountersButton = page.locator("button", {
|
||||
hasText: "Add 1000 Counters",
|
||||
});
|
||||
|
||||
this.clearCountersButton = page.locator("button", {
|
||||
hasText: "Clear Counters",
|
||||
});
|
||||
|
||||
this.decrementCountButton = page.locator("button", {
|
||||
hasText: "-1",
|
||||
});
|
||||
|
||||
this.incrementCountButton = page.locator("button", {
|
||||
hasText: "+1",
|
||||
});
|
||||
|
||||
this.removeCountButton = page.locator("button", {
|
||||
hasText: "x",
|
||||
});
|
||||
|
||||
this.total = page.getByTestId("total");
|
||||
|
||||
this.counters = page.getByTestId("counters");
|
||||
|
||||
this.counterInput = page.getByRole("textbox");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/");
|
||||
}
|
||||
|
||||
async addCounter() {
|
||||
await Promise.all([
|
||||
this.addCounterButton.waitFor(),
|
||||
this.addCounterButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async addOneThousandCounters() {
|
||||
this.addOneThousandCountersButton.click();
|
||||
}
|
||||
|
||||
async decrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.decrementCountButton.nth(index).waitFor(),
|
||||
this.decrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async incrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.incrementCountButton.nth(index).waitFor(),
|
||||
this.incrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearCounters() {
|
||||
await Promise.all([
|
||||
this.clearCountersButton.waitFor(),
|
||||
this.clearCountersButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async enterCount(count: string, index: number = 0) {
|
||||
await Promise.all([
|
||||
this.counterInput.nth(index).waitFor(),
|
||||
this.counterInput.nth(index).fill(count),
|
||||
]);
|
||||
}
|
||||
|
||||
async removeCounter(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.removeCountButton.nth(index).waitFor(),
|
||||
this.removeCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
examples/counters_stable/e2e/tests/increment_count.spec.ts
Normal file
17
examples/counters_stable/e2e/tests/increment_count.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Increment Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("3");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
});
|
||||
18
examples/counters_stable/e2e/tests/remove_counter.spec.ts
Normal file
18
examples/counters_stable/e2e/tests/remove_counter.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Remove Counter", () => {
|
||||
test("should decrement the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.removeCounter(1);
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("2");
|
||||
});
|
||||
});
|
||||
19
examples/counters_stable/e2e/tests/view_counters.spec.ts
Normal file
19
examples/counters_stable/e2e/tests/view_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("View Counters", () => {
|
||||
test("should see the title", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await expect(page).toHaveTitle("Counters (Stable)");
|
||||
});
|
||||
|
||||
test("should see the initial counts", async ({ page }) => {
|
||||
const counters = new CountersPage(page);
|
||||
await counters.goto();
|
||||
|
||||
await expect(counters.total).toHaveText("0");
|
||||
await expect(counters.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Counters (Stable)</title>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
</head>
|
||||
<body></body>
|
||||
|
||||
11
examples/counters_stable/package.json
Normal file
11
examples/counters_stable/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"start-server": "trunk serve",
|
||||
"e2e": "cargo make test-playwright",
|
||||
"e2e:auto-start": "start-server-and-test start-server http://127.0.0.1:8080 e2e"
|
||||
},
|
||||
"devDependencies": {
|
||||
"start-server-and-test": "^1.15.4"
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span>{move ||
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
@@ -64,7 +64,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span>{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
@@ -99,13 +99,13 @@ fn Counter(
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{move || value.get().to_string()}</span>
|
||||
<button on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<span>{value}</span>
|
||||
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -10,12 +10,10 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
reqwasm = "0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
|
||||
@@ -4,5 +4,5 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
gtk = { version = "0.5.0", package = "gtk4" }
|
||||
@@ -16,12 +16,10 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
@@ -14,12 +14,10 @@ lto = true
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features=["template_macro"] }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly", "template_macro"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
# used in rand, but we need to enable js feature
|
||||
|
||||
@@ -11,12 +11,10 @@ axum = { version = "0.6.18", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1.28.1", optional = true }
|
||||
|
||||
@@ -6,6 +6,6 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[patch.crates-io]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router" }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
api-boundary = { path = "api-boundary" }
|
||||
|
||||
@@ -7,8 +7,9 @@ publish = false
|
||||
[dependencies]
|
||||
api-boundary = "*"
|
||||
|
||||
leptos = { version = "0.2.0-alpha2", features = ["stable"] }
|
||||
leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
|
||||
leptos = { path = "../../../leptos", features = ["csr"] }
|
||||
leptos_meta = { path = "../../../meta", features = ["csr"] }
|
||||
leptos_router = { path = "../../../router", features = ["csr"] }
|
||||
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// Button B: pass a closure
|
||||
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
|
||||
|
||||
// Button B: use a regular event listener
|
||||
// Button C: use a regular event listener
|
||||
// setting an event listener on a component like this applies it
|
||||
// to each of the top-level elements the component returns
|
||||
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
|
||||
|
||||
21
examples/router/.gitignore
vendored
Normal file
21
examples/router/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Support trunk
|
||||
dist
|
||||
|
||||
@@ -10,12 +10,12 @@ lto = true
|
||||
[dependencies]
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["csr", "nightly"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr", "nightly"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
|
||||
36
examples/router/e2e/package-lock.json
generated
Normal file
36
examples/router/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "e2e",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.35.1"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "20.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz",
|
||||
"integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==",
|
||||
"dev": true
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
7
examples/router/e2e/package.json
Normal file
7
examples/router/e2e/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
}
|
||||
77
examples/router/e2e/playwright.config.ts
Normal file
77
examples/router/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 10 : 10,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "list",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:8080",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: "cd ../ && trunk serve",
|
||||
// url: "http://127.0.0.1:8080",
|
||||
// reuseExistingServer: false, //!process.env.CI,
|
||||
// },
|
||||
});
|
||||
30
examples/router/e2e/tests/router.spec.ts
Normal file
30
examples/router/e2e/tests/router.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Test Router example", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
const links = [
|
||||
{ label: "Bill Smith", url: "/0" },
|
||||
{ label: "Tim Jones", url: "/1" },
|
||||
{ label: "Sally Stevens", url: "/2" },
|
||||
{ label: "About", url: "/about" },
|
||||
{ label: "Settings", url: "/settings" },
|
||||
];
|
||||
links.forEach(({ label, url }) => {
|
||||
test(`Can navigate to ${label}`, async ({ page }) => {
|
||||
await page.getByRole("link", { name: label }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: label })).toBeVisible();
|
||||
await expect(page).toHaveURL(url);
|
||||
});
|
||||
});
|
||||
|
||||
test("Can redirect to home", async ({ page }) => {
|
||||
await page.getByRole("link", { name: "About" }).click();
|
||||
|
||||
await page.getByRole("link", { name: "Redirect to Home" }).click();
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
||||
});
|
||||
11
examples/router/package.json
Normal file
11
examples/router/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-server": "trunk serve",
|
||||
"e2e": "cargo make test-playwright",
|
||||
"e2e:auto-start": "start-server-and-test start-server http://127.0.0.1:8080 e2e"
|
||||
},
|
||||
"devDependencies": {
|
||||
"start-server-and-test": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,10 @@ rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -13,12 +13,10 @@ console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
|
||||
@@ -11,12 +11,10 @@ console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
|
||||
@@ -10,12 +10,10 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
log = "0.4"
|
||||
cfg-if = "1.0"
|
||||
|
||||
@@ -4,12 +4,9 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { version = "0.2", features = [
|
||||
"serde",
|
||||
"csr",
|
||||
] }
|
||||
leptos_meta = { version = "0.2", features = ["csr"] }
|
||||
leptos_router = { version = "0.2", features = ["csr"] }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr", "nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["csr", "nightly"] }
|
||||
log = "0.4"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
|
||||
@@ -52,8 +52,9 @@ If you're using VS Code, add the following to your `settings.json`
|
||||
|
||||
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
|
||||
|
||||
Install "VS Browser" extension, a browser at the right window.
|
||||
Allow vscode Ports forward: 3000, 3001.
|
||||
Install [VS Browser](https://marketplace.visualstudio.com/items?itemName=Phu1237.vs-browser) extension (allows you to open a browser at the right window.
|
||||
|
||||
Allow vscode Ports forward: 3000, 3001.
|
||||
|
||||
## Notes about Tooling
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -16,12 +16,10 @@ console_error_panic_hook = "0.1.7"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
@@ -32,7 +30,6 @@ sqlx = { version = "0.6.2", features = [
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
|
||||
@@ -11,12 +11,10 @@ console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -33,8 +31,6 @@ thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
|
||||
@@ -11,13 +11,11 @@ console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_viz = { path = "../../integrations/viz", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_viz = { path = "../../integrations/viz", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
leptos_reactive = { path = "../../leptos_reactive", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -32,7 +30,6 @@ thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -1315,6 +1315,12 @@ impl<B> From<Request<B>> for ExtractorHelper {
|
||||
/// .map_err(|e| ServerFnError::ServerError("Could not extract method and query...".to_string()))
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// > 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).
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn extract<T, U>(
|
||||
cx: Scope,
|
||||
|
||||
@@ -13,19 +13,19 @@ cfg-if = "1"
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true, default-features = false }
|
||||
leptos_server = { workspace = true}
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.14"
|
||||
server_fn = { workspace = true, default-features = false }
|
||||
server_fn = { workspace = true}
|
||||
web-sys = { version = "0.3.63", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
leptos = { path = "."}
|
||||
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
default = ["serde"]
|
||||
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]
|
||||
csr = [
|
||||
"leptos_dom/web",
|
||||
@@ -47,11 +47,11 @@ ssr = [
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
]
|
||||
stable = [
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
nightly = [
|
||||
"leptos_dom/nightly",
|
||||
"leptos_macro/nightly",
|
||||
"leptos_reactive/nightly",
|
||||
"leptos_server/nightly",
|
||||
]
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
@@ -60,7 +60,7 @@ rkyv = ["leptos_reactive/rkyv"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable", "tracing", "template_macro", "rustls", "default-tls", "web-sys", "wasm-bindgen"]
|
||||
denylist = ["nightly", "tracing", "template_macro", "rustls", "default-tls", "web-sys", "wasm-bindgen"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
|
||||
@@ -16,14 +16,14 @@ use leptos_reactive::{
|
||||
/// # use leptos_dom::*; use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let (value, set_value) = create_signal(cx, Ok(0));
|
||||
/// let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
/// let on_input = move |ev| set_value.set(event_target_value(&ev).parse::<i32>());
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <input type="text" on:input=on_input/>
|
||||
/// <ErrorBoundary
|
||||
/// fallback=move |_, _| view! { cx, <p class="error">"Enter a valid number."</p>}
|
||||
/// >
|
||||
/// <p>"Value is: " {value}</p>
|
||||
/// <p>"Value is: " {move || value.get()}</p>
|
||||
/// </ErrorBoundary>
|
||||
/// }
|
||||
/// # });
|
||||
|
||||
@@ -26,7 +26,7 @@ use std::hash::Hash;
|
||||
/// <div>
|
||||
/// <For
|
||||
/// // a function that returns the items we're iterating over; a signal is fine
|
||||
/// each=counters
|
||||
/// each=move || counters.get()
|
||||
/// // a unique key for each item
|
||||
/// key=|counter| counter.id
|
||||
/// // renders each item to a view
|
||||
|
||||
@@ -16,14 +16,6 @@
|
||||
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
|
||||
//! Explore our [Examples](https://github.com/leptos-rs/leptos/tree/main/examples) to see Leptos in action.
|
||||
//!
|
||||
//! # `nightly` Note
|
||||
//! Most of the examples assume you’re using `nightly` Rust. If you’re on stable, note the following:
|
||||
//! 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`
|
||||
//! 2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
//! you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
//! [`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
//! for examples of the correct API.
|
||||
//!
|
||||
//! # Learning by Example
|
||||
//!
|
||||
//! If you want to see what Leptos is capable of, check out
|
||||
@@ -84,12 +76,10 @@
|
||||
//! - **Server Functions**: the [server](crate::leptos_server) macro, [create_action], and [create_server_action]
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//! - `csr` (*Default*) Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
|
||||
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
|
||||
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
|
||||
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
|
||||
//! of calling signals as functions. If you need to use `stable`, you will need to call `.get()`
|
||||
//! and `.set()` manually.
|
||||
//! - `serde` (*Default*) In SSR/hydrate mode, uses [serde](https://docs.rs/serde/latest/serde/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
|
||||
@@ -122,7 +112,7 @@
|
||||
//! <div>
|
||||
//! <button on:click=clear>"Clear"</button>
|
||||
//! <button on:click=decrement>"-1"</button>
|
||||
//! <span>"Value: " {move || value().to_string()} "!"</span>
|
||||
//! <span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
//! <button on:click=increment>"+1"</button>
|
||||
//! </div>
|
||||
//! }
|
||||
|
||||
@@ -21,7 +21,7 @@ use std::{cell::RefCell, rc::Rc};
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <Show
|
||||
/// when=move || value() < 5
|
||||
/// when=move || value.get() < 5
|
||||
/// fallback=|cx| view! { cx, "Big number!" }
|
||||
/// >
|
||||
/// "Small number!"
|
||||
|
||||
@@ -21,7 +21,7 @@ use std::rc::Rc;
|
||||
///
|
||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
///
|
||||
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
/// let cats = create_resource(cx, move || cat_count.get(), |count| fetch_cats(count));
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <div>
|
||||
@@ -73,7 +73,7 @@ where
|
||||
|
||||
let child = DynChild::new({
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
let current_id = current_id.clone();
|
||||
let current_id = current_id;
|
||||
|
||||
let children = Rc::new(orig_children(cx).into_view(cx));
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
@@ -94,10 +94,10 @@ where
|
||||
// run the child; we'll probably throw this away, but it will register resource reads
|
||||
//let after_original_child = HydrationCtx::peek();
|
||||
|
||||
let initial = {
|
||||
{
|
||||
// no resources were read under this, so just return the child
|
||||
if context.pending_resources.get() == 0 {
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
HydrationCtx::continue_from(current_id);
|
||||
DynChild::new({
|
||||
let children = Rc::clone(&children);
|
||||
move || (*children).clone()
|
||||
@@ -115,9 +115,7 @@ where
|
||||
{
|
||||
let orig_children = Rc::clone(&orig_children);
|
||||
move || {
|
||||
HydrationCtx::continue_from(
|
||||
current_id.clone(),
|
||||
);
|
||||
HydrationCtx::continue_from(current_id);
|
||||
DynChild::new({
|
||||
let orig_children =
|
||||
orig_children(cx).into_view(cx);
|
||||
@@ -132,9 +130,7 @@ where
|
||||
{
|
||||
let orig_children = Rc::clone(&orig_children);
|
||||
move || {
|
||||
HydrationCtx::continue_from(
|
||||
current_id.clone(),
|
||||
);
|
||||
HydrationCtx::continue_from(current_id);
|
||||
DynChild::new({
|
||||
let orig_children =
|
||||
orig_children(cx).into_view(cx);
|
||||
@@ -149,9 +145,7 @@ where
|
||||
// return the fallback for now, wrapped in fragment identifier
|
||||
fallback().into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
initial
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,7 +32,8 @@ use std::{
|
||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
/// let (pending, set_pending) = create_signal(cx, false);
|
||||
///
|
||||
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
/// let cats =
|
||||
/// create_resource(cx, move || cat_count.get(), |count| fetch_cats(count));
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <div>
|
||||
@@ -64,7 +65,7 @@ use std::{
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
)]
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Transition<F, E>(
|
||||
cx: Scope,
|
||||
/// Will be displayed while resources are pending.
|
||||
|
||||
@@ -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 = "../../..", default-features = false, features = ["serde"] }
|
||||
leptos = { path = "../../..", features = ["serde"] }
|
||||
leptos_actix = { path = "../../../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../../../router", default-features = false }
|
||||
leptos_router = { path = "../../../../router"}
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
wasm-bindgen = "0.2.87"
|
||||
|
||||
@@ -153,16 +153,30 @@ impl TryFrom<String> for Env {
|
||||
/// If an env var is specified, like `LEPTOS_ENV`, it will override a setting in the file.
|
||||
pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
|
||||
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
|
||||
let start = match re.find(text) {
|
||||
Some(found) => found.start(),
|
||||
None => return Err(LeptosConfigError::ConfigSectionNotFound),
|
||||
let re_workspace: Regex =
|
||||
Regex::new(r#"(?m)^\[\[workspace.metadata.leptos\]\]"#).unwrap();
|
||||
|
||||
let metadata_name;
|
||||
let start;
|
||||
match re.find(text) {
|
||||
Some(found) => {
|
||||
metadata_name = "[package.metadata.leptos]";
|
||||
start = found.start();
|
||||
}
|
||||
None => match re_workspace.find(text) {
|
||||
Some(found) => {
|
||||
metadata_name = "[[workspace.metadata.leptos]]";
|
||||
start = found.start();
|
||||
}
|
||||
None => return Err(LeptosConfigError::ConfigSectionNotFound),
|
||||
},
|
||||
};
|
||||
|
||||
// so that serde error messages have right line number
|
||||
let newlines = text[..start].matches('\n').count();
|
||||
let input = "\n".repeat(newlines) + &text[start..];
|
||||
let toml = input
|
||||
.replace("[package.metadata.leptos]", "[leptos_options]")
|
||||
.replace(metadata_name, "[leptos_options]")
|
||||
.replace('-', "_");
|
||||
let settings = Config::builder()
|
||||
// Read the "default" configuration file
|
||||
|
||||
@@ -159,8 +159,8 @@ features = [
|
||||
default = []
|
||||
web = ["leptos_reactive/csr"]
|
||||
ssr = ["leptos_reactive/ssr"]
|
||||
stable = ["leptos_reactive/stable"]
|
||||
nightly = ["leptos_reactive/nightly"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
denylist = ["nightly"]
|
||||
skip_feature_sets = [["web", "ssr"]]
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../leptos", default-features = false }
|
||||
leptos = { path = "../../../leptos", features = ["nightly"] }
|
||||
actix-web = { version = "4", optional = true }
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
gloo = { version = "0.8", features = ["futures"] }
|
||||
leptos = { path = "../../../leptos", features = ["tracing"] }
|
||||
leptos = { path = "../../../leptos", features = ["nightly", "csr", "tracing"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
tracing-subscriber-wasm = "0.1"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -271,8 +271,9 @@ where
|
||||
let mut repr = ComponentRepr::new_with_id(name, id);
|
||||
|
||||
// disposed automatically when the parent scope is disposed
|
||||
let (child, _) = cx
|
||||
.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
|
||||
let (child, _) = cx.run_child_scope(|cx| {
|
||||
cx.untrack_with_diagnostics(|| children_fn(cx).into_view(cx))
|
||||
});
|
||||
|
||||
repr.children.push(child);
|
||||
|
||||
|
||||
@@ -822,7 +822,11 @@ fn apply_diff<T, EF, V>(
|
||||
#[cfg(not(debug_assertions))]
|
||||
parent.append_with_node_1(closing).unwrap();
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
range.set_start_after(opening).unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
range.set_start_before(opening).unwrap();
|
||||
|
||||
range.set_end_before(closing).unwrap();
|
||||
|
||||
range.delete_contents().unwrap();
|
||||
|
||||
@@ -733,22 +733,24 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
|
||||
);
|
||||
|
||||
let mut new_classes = classes
|
||||
let new_classes = classes
|
||||
.iter()
|
||||
.flat_map(|classes| classes.split_whitespace());
|
||||
|
||||
if let Some(prev_classes) = prev_classes {
|
||||
let new_classes =
|
||||
new_classes.collect::<SmallVec<[_; 4]>>();
|
||||
let mut old_classes = prev_classes
|
||||
.iter()
|
||||
.flat_map(|classes| classes.split_whitespace());
|
||||
|
||||
// Remove old classes
|
||||
for prev_class in old_classes.clone() {
|
||||
if !new_classes.any(|c| c == prev_class) {
|
||||
if !new_classes.iter().any(|c| c == &prev_class) {
|
||||
class_list.remove_1(prev_class).unwrap_or_else(
|
||||
|err| {
|
||||
panic!(
|
||||
"failed to add class \
|
||||
"failed to remove class \
|
||||
`{prev_class}`, error: {err:#?}"
|
||||
)
|
||||
},
|
||||
@@ -761,7 +763,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
if !old_classes.any(|c| c == class) {
|
||||
class_list.add_1(class).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"failed to remove class `{class}`, \
|
||||
"failed to add class `{class}`, \
|
||||
error: {err:#?}"
|
||||
)
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![deny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||
|
||||
//! The DOM implementation for `leptos`.
|
||||
|
||||
@@ -33,7 +33,7 @@ pub use html::HtmlElement;
|
||||
use html::{AnyElement, ElementDescriptor};
|
||||
pub use hydration::{HydrationCtx, HydrationKey};
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(feature = "stable")]
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
@@ -143,7 +143,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stable")]
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoView for ReadSignal<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
@@ -156,7 +156,7 @@ where
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stable")]
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoView for RwSignal<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
@@ -169,7 +169,7 @@ where
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stable")]
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoView for Memo<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
@@ -182,7 +182,7 @@ where
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stable")]
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoView for Signal<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
@@ -195,7 +195,7 @@ where
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stable")]
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoView for MaybeSignal<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
@@ -1125,7 +1125,7 @@ viewable_primitive![
|
||||
];
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
if #[cfg(feature = "nightly")] {
|
||||
viewable_primitive! {
|
||||
std::backtrace::Backtrace
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ impl<T: ElementDescriptor> Clone for NodeRef<T> {
|
||||
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
if #[cfg(feature = "nightly")] {
|
||||
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
|
||||
type Output = Option<HtmlElement<T>>;
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ fn ooo_body_stream_recurse(
|
||||
fragments.chain(resources).chain(
|
||||
futures::stream::once(async move {
|
||||
let pending = cx.pending_fragments();
|
||||
if pending.len() > 0 {
|
||||
if !pending.is_empty() {
|
||||
let fragments = FuturesUnordered::new();
|
||||
let serializers = cx.serialization_resolvers();
|
||||
for (fragment_id, data) in pending {
|
||||
|
||||
@@ -70,7 +70,7 @@ impl ViewMacros {
|
||||
let mut views = Vec::new();
|
||||
for view in visitor.views {
|
||||
let span = view.span();
|
||||
let id = span_to_stable_id(path, span);
|
||||
let id = span_to_stable_id(path, span.start().line);
|
||||
let mut tokens = view.tokens.clone().into_iter();
|
||||
tokens.next(); // cx
|
||||
tokens.next(); // ,
|
||||
@@ -148,15 +148,11 @@ impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_to_stable_id(
|
||||
path: impl AsRef<Path>,
|
||||
site: proc_macro2::Span,
|
||||
) -> String {
|
||||
pub fn span_to_stable_id(path: impl AsRef<Path>, line: usize) -> String {
|
||||
let file = path
|
||||
.as_ref()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.replace(['/', '\\'], "-");
|
||||
let start = site.start();
|
||||
format!("{}-{:?}", file, start.line)
|
||||
format!("{file}-{line}")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ function patch(json) {
|
||||
try {
|
||||
const views = JSON.parse(json);
|
||||
for (const [id, patches] of views) {
|
||||
console.log("[HOT RELOAD]", patches);
|
||||
console.log("[HOT RELOAD]", id, patches);
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
|
||||
open = `leptos-view|${id}|open`,
|
||||
close = `leptos-view|${id}|close`;
|
||||
|
||||
@@ -39,9 +39,9 @@ default = ["ssr"]
|
||||
csr = []
|
||||
hydrate = []
|
||||
ssr = []
|
||||
stable = ["server_fn_macro/stable"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable", "tracing"]
|
||||
denylist = ["nightly", "tracing"]
|
||||
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
|
||||
|
||||
@@ -438,15 +438,18 @@ impl Docs {
|
||||
let mut attrs = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
let Meta::NameValue(attr ) = &attr.meta else {
|
||||
return None
|
||||
let Meta::NameValue(attr) = &attr.meta else {
|
||||
return None;
|
||||
};
|
||||
if !attr.path.is_ident("doc") {
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(val) = value_to_string(&attr.value) else {
|
||||
abort!(attr, "expected string literal in value of doc comment");
|
||||
abort!(
|
||||
attr,
|
||||
"expected string literal in value of doc comment"
|
||||
);
|
||||
};
|
||||
|
||||
Some((val, attr.path.span()))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
|
||||
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#[macro_use]
|
||||
@@ -94,7 +94,7 @@ mod template;
|
||||
/// Attributes can take a wide variety of primitive types that can be converted to strings. They can also
|
||||
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
@@ -102,11 +102,11 @@ mod template;
|
||||
///
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// // ❌ not like this: `count()` returns an `i32`, not a function
|
||||
/// <p>{count()}</p>
|
||||
/// // ❌ not like this: `count.get()` returns an `i32`, not a function
|
||||
/// <p>{count.get()}</p>
|
||||
/// // ✅ this is good: Leptos sees the function and knows it's a dynamic value
|
||||
/// <p>{move || count.get()}</p>
|
||||
/// // 🔥 `count` is itself a function, so you can pass it directly (unless you're on `stable`)
|
||||
/// // 🔥 with the `nightly` feature, `count` is a function, so `count` itself can be passed directly into the view
|
||||
/// <p>{count}</p>
|
||||
/// }
|
||||
/// # ;
|
||||
@@ -147,9 +147,9 @@ mod template;
|
||||
/// <input
|
||||
/// type="text"
|
||||
/// name="user_name"
|
||||
/// value={name} // this only sets the default value!
|
||||
/// prop:value={name} // here's how you update values. Sorry, I didn’t invent the DOM.
|
||||
/// on:click=move |ev| set_name(event_target_value(&ev)) // `event_target_value` is a useful little Leptos helper
|
||||
/// value={move || name.get()} // this only sets the default value!
|
||||
/// prop:value={move || name.get()} // here's how you update values. Sorry, I didn’t invent the DOM.
|
||||
/// on:click=move |ev| set_name.set(event_target_value(&ev)) // `event_target_value` is a useful little Leptos helper
|
||||
/// />
|
||||
/// }
|
||||
/// # ;
|
||||
@@ -163,7 +163,7 @@ mod template;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 2);
|
||||
/// view! { cx, <div class:hidden-div={move || count() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// view! { cx, <div class:hidden-div={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
@@ -176,7 +176,7 @@ mod template;
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 2);
|
||||
/// // `hidden-div-25` is invalid at the moment
|
||||
/// view! { cx, <div class:hidden-div-25={move || count() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// view! { cx, <div class:hidden-div-25={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
@@ -191,7 +191,7 @@ mod template;
|
||||
/// // this allows you to use CSS frameworks that include complex class names
|
||||
/// view! { cx,
|
||||
/// <div
|
||||
/// class=("is-[this_-_really]-necessary-42", move || count() < 3)
|
||||
/// class=("is-[this_-_really]-necessary-42", move || count.get() < 3)
|
||||
/// >
|
||||
/// "Now you see me, now you don’t."
|
||||
/// </div>
|
||||
@@ -211,9 +211,9 @@ mod template;
|
||||
/// view! { cx,
|
||||
/// <div
|
||||
/// style="position: absolute"
|
||||
/// style:left=move || format!("{}px", x())
|
||||
/// style:top=move || format!("{}px", y())
|
||||
/// style=("background-color", move || format!("rgb({}, {}, 100)", x(), y()))
|
||||
/// style:left=move || format!("{}px", x.get())
|
||||
/// style:top=move || format!("{}px", y.get())
|
||||
/// style=("background-color", move || format!("rgb({}, {}, 100)", x.get(), y.get()))
|
||||
/// >
|
||||
/// "Moves when coordinates change"
|
||||
/// </div>
|
||||
@@ -285,7 +285,7 @@ mod template;
|
||||
///
|
||||
/// // create event handlers for our buttons
|
||||
/// // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
/// let clear = move |_ev| set_value(0);
|
||||
/// let clear = move |_ev| set_value.set(0);
|
||||
/// let decrement = move |_ev| set_value.update(|value| *value -= 1);
|
||||
/// let increment = move |_ev| set_value.update(|value| *value += 1);
|
||||
///
|
||||
@@ -295,7 +295,7 @@ mod template;
|
||||
/// <div>
|
||||
/// <button on:click=clear>"Clear"</button>
|
||||
/// <button on:click=decrement>"-1"</button>
|
||||
/// <span>"Value: " {move || value().to_string()} "!"</span>
|
||||
/// <span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
/// <button on:click=increment>"+1"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
@@ -381,10 +381,10 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
|
||||
fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(debug_assertions, not(feature = "stable")))] {
|
||||
if #[cfg(all(debug_assertions, feature = "nightly"))] {
|
||||
Some(leptos_hot_reload::span_to_stable_id(
|
||||
site.source_file().path(),
|
||||
site.into()
|
||||
site.start().line()
|
||||
))
|
||||
} else {
|
||||
_ = site;
|
||||
@@ -467,7 +467,7 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
/// // return the user interface, which will be automatically updated
|
||||
/// // when signal values change
|
||||
/// view! { cx,
|
||||
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
|
||||
/// <p>"Your name is " {name} " and you are " {move || age.get()} " years old."</p>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@@ -793,15 +793,18 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
|
||||
/// are enabled), it will instead make a network request to the server.
|
||||
///
|
||||
/// You can specify one, two, or three arguments to the server function:
|
||||
/// You can specify one, two, three, or four arguments to the server function:
|
||||
/// 1. **Required**: A type name that will be used to identify and register the server function
|
||||
/// (e.g., `MyServerFn`).
|
||||
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
|
||||
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
|
||||
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
|
||||
/// work without WebAssembly, the encoding must be `"Url"`.
|
||||
/// 3. *Optional*: The encoding for the server function (`"Url"`, `"Cbor"`, `"GetJson"`, or `"GetCbor`". See **Server Function Encodings** below.)
|
||||
/// 4. *Optional*: A specific endpoint path to be used in the URL. (By default, a unique path will be generated.)
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // will generate a server function at `/api-prefix/hello`
|
||||
/// #[server(MyServerFnType, "/api-prefix", "Url", "hello")]
|
||||
/// ```
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos
|
||||
@@ -821,17 +824,16 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
///
|
||||
/// Note the following:
|
||||
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must be [Serializable](https://docs.rs/leptos/latest/leptos/trait.Serializable.html).**
|
||||
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
|
||||
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
/// need to deserialize the result to return it to the client.
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// - **Arguments must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
@@ -840,6 +842,9 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a Leptos `Scope`. This scope can be used to inject dependencies like the HTTP request
|
||||
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
/// - Your server must be ready to handle the server functions at the API prefix you list. The easiest way to do this
|
||||
/// is to use the `handle_server_fns` function from [`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/fn.handle_server_fns.html)
|
||||
/// or [`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.handle_server_fns.html).
|
||||
///
|
||||
/// ## Server Function Encodings
|
||||
///
|
||||
|
||||
@@ -581,7 +581,14 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
|| name.strip_prefix("style:").is_some()
|
||||
{
|
||||
// ignore props for SSR
|
||||
// ignore classes and sdtyles: we'll handle these separately
|
||||
// ignore classes and styles: we'll handle these separately
|
||||
if name.starts_with("prop:") {
|
||||
let value = attr.value();
|
||||
exprs_for_compiler.push(quote! {
|
||||
#[allow(unused_braces)]
|
||||
{ _ = #value; }
|
||||
});
|
||||
}
|
||||
} else if name == "inner_html" {
|
||||
return attr.value();
|
||||
} else {
|
||||
@@ -606,7 +613,9 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
if let Some(value) = value_to_string(value) {
|
||||
template.push_str(&name);
|
||||
template.push_str("=\"");
|
||||
template.push_str(&value);
|
||||
template.push_str(&html_escape::encode_quoted_attribute(
|
||||
&value,
|
||||
));
|
||||
template.push('"');
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
@@ -729,7 +738,9 @@ fn set_class_attribute_ssr(
|
||||
{
|
||||
template.push_str(" class=\"");
|
||||
|
||||
template.push_str(&static_class_attr);
|
||||
template.push_str(&html_escape::encode_quoted_attribute(
|
||||
&static_class_attr,
|
||||
));
|
||||
|
||||
for (_span, value) in dyn_class_attr {
|
||||
if let Some(value) = value {
|
||||
@@ -1363,8 +1374,8 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) {
|
||||
let is_custom = event_type == "Custom";
|
||||
|
||||
let Ok(event_type) = event_type.parse::<TokenStream>() else {
|
||||
abort!(event_type, "couldn't parse event name");
|
||||
};
|
||||
abort!(event_type, "couldn't parse event name");
|
||||
};
|
||||
|
||||
let event_type = if is_custom {
|
||||
quote! { Custom::new(#name) }
|
||||
@@ -1393,7 +1404,10 @@ pub(crate) fn slot_to_tokens(
|
||||
let span = node.name().span();
|
||||
|
||||
let Some(parent_slots) = parent_slots else {
|
||||
proc_macro_error::emit_error!(span, "slots cannot be used inside HTML elements");
|
||||
proc_macro_error::emit_error!(
|
||||
span,
|
||||
"slots cannot be used inside HTML elements"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -68,14 +68,14 @@ hydrate = [
|
||||
"dep:web-sys",
|
||||
]
|
||||
ssr = ["dep:tokio"]
|
||||
stable = []
|
||||
nightly = []
|
||||
serde = []
|
||||
serde-lite = ["dep:serde-lite"]
|
||||
miniserde = ["dep:miniserde"]
|
||||
rkyv = ["dep:rkyv", "dep:bytecheck"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
denylist = ["nightly"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
|
||||
@@ -29,20 +29,20 @@ use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
/// // ✅ use effects to interact between reactive state and the outside world
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // immediately prints "Value: 0" and subscribes to `a`
|
||||
/// log::debug!("Value: {}", a());
|
||||
/// log::debug!("Value: {}", a.get());
|
||||
/// });
|
||||
///
|
||||
/// set_a(1);
|
||||
/// set_a.set(1);
|
||||
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
|
||||
///
|
||||
/// // ❌ don't use effects to synchronize state within the reactive system
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // this technically works but can cause unnecessary re-renders
|
||||
/// // and easily lead to problems like infinite loops
|
||||
/// set_b(a() + 1);
|
||||
/// set_b.set(a.get() + 1);
|
||||
/// });
|
||||
/// # if !cfg!(feature = "ssr") {
|
||||
/// # assert_eq!(b(), 2);
|
||||
/// # assert_eq!(b.get(), 2);
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
@@ -88,19 +88,19 @@ where
|
||||
/// // ✅ use effects to interact between reactive state and the outside world
|
||||
/// create_isomorphic_effect(cx, move |_| {
|
||||
/// // immediately prints "Value: 0" and subscribes to `a`
|
||||
/// log::debug!("Value: {}", a());
|
||||
/// log::debug!("Value: {}", a.get());
|
||||
/// });
|
||||
///
|
||||
/// set_a(1);
|
||||
/// set_a.set(1);
|
||||
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
|
||||
///
|
||||
/// // ❌ don't use effects to synchronize state within the reactive system
|
||||
/// create_isomorphic_effect(cx, move |_| {
|
||||
/// // this technically works but can cause unnecessary re-renders
|
||||
/// // and easily lead to problems like infinite loops
|
||||
/// set_b(a() + 1);
|
||||
/// set_b.set(a.get() + 1);
|
||||
/// });
|
||||
/// # assert_eq!(b(), 2);
|
||||
/// # assert_eq!(b.get(), 2);
|
||||
/// # }).dispose();
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature="ssr"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
|
||||
|
||||
//! The reactive system for the [Leptos](https://docs.rs/leptos/latest/leptos/) Web framework.
|
||||
//!
|
||||
@@ -44,25 +44,28 @@
|
||||
//! let (count, set_count) = create_signal(cx, 0);
|
||||
//!
|
||||
//! // calling the getter gets the value
|
||||
//! assert_eq!(count(), 0);
|
||||
//! // can be `count()` on nightly
|
||||
//! assert_eq!(count.get(), 0);
|
||||
//! // calling the setter sets the value
|
||||
//! set_count(1);
|
||||
//! // can be `set_count(1)` on nightly
|
||||
//! set_count.set(1);
|
||||
//! // or we can mutate it in place with update()
|
||||
//! set_count.update(|n| *n += 1);
|
||||
//!
|
||||
//! // a derived signal: a plain closure that relies on the signal
|
||||
//! // the closure will run whenever we *access* double_count()
|
||||
//! let double_count = move || count() * 2;
|
||||
//! let double_count = move || count.get() * 2;
|
||||
//! assert_eq!(double_count(), 4);
|
||||
//!
|
||||
//! // a memo: subscribes to the signal
|
||||
//! // the closure will run only when count changes
|
||||
//! let memoized_triple_count = create_memo(cx, move |_| count() * 3);
|
||||
//! assert_eq!(memoized_triple_count(), 6);
|
||||
//! let memoized_triple_count = create_memo(cx, move |_| count.get() * 3);
|
||||
//! // can be `memoized_triple_count()` on nightly
|
||||
//! assert_eq!(memoized_triple_count.get(), 6);
|
||||
//!
|
||||
//! // this effect will run whenever count() changes
|
||||
//! // this effect will run whenever `count` changes
|
||||
//! create_effect(cx, move |_| {
|
||||
//! println!("Count = {}", count());
|
||||
//! println!("Count = {}", count.get());
|
||||
//! });
|
||||
//! });
|
||||
//! ```
|
||||
@@ -92,6 +95,7 @@ mod spawn_microtask;
|
||||
mod stored_value;
|
||||
pub mod suspense;
|
||||
mod trigger;
|
||||
mod watch;
|
||||
|
||||
pub use context::*;
|
||||
pub use diagnostics::SpecialNonReactiveZone;
|
||||
@@ -113,6 +117,7 @@ pub use spawn_microtask::*;
|
||||
pub use stored_value::*;
|
||||
pub use suspense::{GlobalSuspenseContext, SuspenseContext};
|
||||
pub use trigger::*;
|
||||
pub use watch::*;
|
||||
|
||||
mod macros {
|
||||
macro_rules! debug_warn {
|
||||
|
||||
@@ -40,12 +40,12 @@ use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
///
|
||||
/// // 🆗 we could create a derived signal with a simple function
|
||||
/// let double_value = move || value() * 2;
|
||||
/// set_value(2);
|
||||
/// let double_value = move || value.get() * 2;
|
||||
/// set_value.set(2);
|
||||
/// assert_eq!(double_value(), 4);
|
||||
///
|
||||
/// // but imagine the computation is really expensive
|
||||
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
|
||||
/// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // 🆗 run #1: calls `really_expensive_computation` the first time
|
||||
/// log::debug!("expensive = {}", expensive());
|
||||
@@ -58,14 +58,15 @@ use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
|
||||
///
|
||||
/// // instead, we create a memo
|
||||
/// // 🆗 run #1: the calculation runs once immediately
|
||||
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
|
||||
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value.get()));
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // 🆗 reads the current value of the memo
|
||||
/// log::debug!("memoized = {}", memoized());
|
||||
/// // 🆗 reads the current value of the memo
|
||||
/// // can be `memoized()` on nightly
|
||||
/// log::debug!("memoized = {}", memoized.get());
|
||||
/// });
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // ✅ reads the current value **without re-running the calculation**
|
||||
/// let value = memoized();
|
||||
/// let value = memoized.get();
|
||||
/// // do something else...
|
||||
/// });
|
||||
/// # }).dispose();
|
||||
@@ -131,12 +132,12 @@ where
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
///
|
||||
/// // 🆗 we could create a derived signal with a simple function
|
||||
/// let double_value = move || value() * 2;
|
||||
/// set_value(2);
|
||||
/// let double_value = move || value.get() * 2;
|
||||
/// set_value.set(2);
|
||||
/// assert_eq!(double_value(), 4);
|
||||
///
|
||||
/// // but imagine the computation is really expensive
|
||||
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
|
||||
/// let expensive = move || really_expensive_computation(value.get()); // lazy: doesn't run until called
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // 🆗 run #1: calls `really_expensive_computation` the first time
|
||||
/// log::debug!("expensive = {}", expensive());
|
||||
@@ -149,14 +150,15 @@ where
|
||||
///
|
||||
/// // instead, we create a memo
|
||||
/// // 🆗 run #1: the calculation runs once immediately
|
||||
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
|
||||
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value.get()));
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // 🆗 reads the current value of the memo
|
||||
/// log::debug!("memoized = {}", memoized());
|
||||
/// log::debug!("memoized = {}", memoized.get());
|
||||
/// });
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // ✅ reads the current value **without re-running the calculation**
|
||||
/// let value = memoized();
|
||||
/// // can be `memoized()` on nightly
|
||||
/// let value = memoized.get();
|
||||
/// // do something else...
|
||||
/// });
|
||||
/// # }).dispose();
|
||||
@@ -327,13 +329,14 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
||||
/// # use leptos_reactive::*;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (count, set_count) = create_signal(cx, 0);
|
||||
/// let double_count = create_memo(cx, move |_| count() * 2);
|
||||
/// let double_count = create_memo(cx, move |_| count.get() * 2);
|
||||
///
|
||||
/// assert_eq!(double_count.get(), 0);
|
||||
/// set_count(1);
|
||||
/// set_count.set(1);
|
||||
///
|
||||
/// // double_count() is shorthand for double_count.get()
|
||||
/// assert_eq!(double_count(), 2);
|
||||
/// // can be `double_count()` on nightly
|
||||
/// // assert_eq!(double_count(), 2);
|
||||
/// assert_eq!(double_count.get(), 2);
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
|
||||
@@ -51,7 +51,7 @@ use std::{
|
||||
/// # // `csr`, `hydrate`, and `ssr` all have issues here
|
||||
/// # // because we're not running in a browser or in Tokio. Let's just ignore it.
|
||||
/// # if false {
|
||||
/// let cats = create_resource(cx, how_many_cats, fetch_cat_picture_urls);
|
||||
/// let cats = create_resource(cx, move || how_many_cats.get(), fetch_cat_picture_urls);
|
||||
///
|
||||
/// // when we read the signal, it contains either
|
||||
/// // 1) None (if the Future isn't ready yet) or
|
||||
@@ -59,7 +59,7 @@ use std::{
|
||||
/// assert_eq!(cats.read(cx), Some(vec!["1".to_string()]));
|
||||
///
|
||||
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
|
||||
/// set_how_many_cats(2);
|
||||
/// set_how_many_cats.set(2);
|
||||
/// assert_eq!(cats.read(cx), Some(vec!["2".to_string()]));
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
@@ -696,7 +696,7 @@ impl<S, T> SignalSet<T> for Resource<S, T> {
|
||||
/// # // `csr`, `hydrate`, and `ssr` all have issues here
|
||||
/// # // because we're not running in a browser or in Tokio. Let's just ignore it.
|
||||
/// # if false {
|
||||
/// let cats = create_resource(cx, how_many_cats, fetch_cat_picture_urls);
|
||||
/// let cats = create_resource(cx, move || how_many_cats.get(), fetch_cat_picture_urls);
|
||||
///
|
||||
/// // when we read the signal, it contains either
|
||||
/// // 1) None (if the Future isn't ready yet) or
|
||||
@@ -704,7 +704,7 @@ impl<S, T> SignalSet<T> for Resource<S, T> {
|
||||
/// assert_eq!(cats.read(cx), Some(vec!["1".to_string()]));
|
||||
///
|
||||
/// // when the signal's value changes, the `Resource` will generate and run a new `Future`
|
||||
/// set_how_many_cats(2);
|
||||
/// set_how_many_cats.set(2);
|
||||
/// assert_eq!(cats.read(cx), Some(vec!["2".to_string()]));
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use crate::{
|
||||
hydration::SharedContext,
|
||||
node::{NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType},
|
||||
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
|
||||
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
|
||||
ScopeProperty, SerializableResource, StoredValueId, Trigger,
|
||||
UnserializableResource, WriteSignal,
|
||||
ScopeProperty, SerializableResource, SpecialNonReactiveZone, StoredValueId,
|
||||
Trigger, UnserializableResource, WriteSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use core::hash::BuildHasherDefault;
|
||||
@@ -358,6 +359,7 @@ impl Debug for Runtime {
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the selected runtime from the thread-local set of runtimes. On the server,
|
||||
/// this will return the correct runtime. In the browser, there should only be one runtime.
|
||||
#[cfg_attr(
|
||||
@@ -472,6 +474,43 @@ impl RuntimeId {
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub(crate) fn untrack<T>(
|
||||
self,
|
||||
f: impl FnOnce() -> T,
|
||||
diagnostics: bool,
|
||||
) -> T {
|
||||
with_runtime(self, |runtime| {
|
||||
let untracked_result;
|
||||
|
||||
if !diagnostics {
|
||||
SpecialNonReactiveZone::enter();
|
||||
}
|
||||
|
||||
let prev_observer =
|
||||
SetObserverOnDrop(self, runtime.observer.take());
|
||||
|
||||
untracked_result = f();
|
||||
|
||||
runtime.observer.set(prev_observer.1);
|
||||
std::mem::forget(prev_observer); // avoid Drop
|
||||
|
||||
if !diagnostics {
|
||||
SpecialNonReactiveZone::exit();
|
||||
}
|
||||
|
||||
untracked_result
|
||||
})
|
||||
.expect(
|
||||
"tried to run untracked function in a runtime that has been \
|
||||
disposed",
|
||||
)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)] // only because it's placed here to fit in with the other create methods
|
||||
pub(crate) fn create_trigger(self) -> Trigger {
|
||||
@@ -681,6 +720,81 @@ impl RuntimeId {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn watch<W, T>(
|
||||
self,
|
||||
deps: impl Fn() -> W + 'static,
|
||||
callback: impl Fn(&W, Option<&W>, Option<T>) -> T + Clone + 'static,
|
||||
immediate: bool,
|
||||
) -> (NodeId, impl Fn() + Clone)
|
||||
where
|
||||
W: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let cur_deps_value = Rc::new(RefCell::new(None::<W>));
|
||||
let prev_deps_value = Rc::new(RefCell::new(None::<W>));
|
||||
let prev_callback_value = Rc::new(RefCell::new(None::<T>));
|
||||
|
||||
let wrapped_callback = {
|
||||
let cur_deps_value = Rc::clone(&cur_deps_value);
|
||||
let prev_deps_value = Rc::clone(&prev_deps_value);
|
||||
let prev_callback_value = Rc::clone(&prev_callback_value);
|
||||
|
||||
move || {
|
||||
callback(
|
||||
cur_deps_value.borrow().as_ref().expect(
|
||||
"this will not be called before there is deps value",
|
||||
),
|
||||
prev_deps_value.borrow().as_ref(),
|
||||
prev_callback_value.take(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let effect_fn = {
|
||||
let prev_callback_value = Rc::clone(&prev_callback_value);
|
||||
|
||||
move |did_run_before: Option<()>| {
|
||||
let deps_value = deps();
|
||||
|
||||
let did_run_before = did_run_before.is_some();
|
||||
|
||||
if !immediate && !did_run_before {
|
||||
prev_deps_value.replace(Some(deps_value));
|
||||
return;
|
||||
}
|
||||
|
||||
cur_deps_value.replace(Some(deps_value.clone()));
|
||||
|
||||
let callback_value =
|
||||
Some(self.untrack(wrapped_callback.clone(), false));
|
||||
|
||||
prev_callback_value.replace(callback_value);
|
||||
|
||||
prev_deps_value.replace(Some(deps_value));
|
||||
}
|
||||
};
|
||||
|
||||
let id = self.create_concrete_effect(
|
||||
Rc::new(RefCell::new(None::<()>)),
|
||||
Rc::new(Effect {
|
||||
f: effect_fn,
|
||||
ty: PhantomData,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}),
|
||||
);
|
||||
|
||||
(id, move || {
|
||||
with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().remove(id);
|
||||
runtime.node_sources.borrow_mut().remove(id);
|
||||
})
|
||||
.expect(
|
||||
"tried to stop a watch in a runtime that has been disposed",
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub(crate) fn create_memo<T>(
|
||||
@@ -828,3 +942,13 @@ impl std::hash::Hash for Runtime {
|
||||
std::ptr::hash(&self, state);
|
||||
}
|
||||
}
|
||||
|
||||
struct SetObserverOnDrop(RuntimeId, Option<NodeId>);
|
||||
|
||||
impl Drop for SetObserverOnDrop {
|
||||
fn drop(&mut self) {
|
||||
_ = with_runtime(self.0, |rt| {
|
||||
rt.observer.set(self.1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ use crate::{
|
||||
node::NodeId,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
suspense::StreamChunk,
|
||||
PinnedFuture, ResourceId, SpecialNonReactiveZone, StoredValueId,
|
||||
SuspenseContext,
|
||||
PinnedFuture, ResourceId, StoredValueId, SuspenseContext,
|
||||
};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use std::{
|
||||
@@ -189,17 +188,17 @@ impl Scope {
|
||||
/// let (b, set_b) = create_signal(cx, 0);
|
||||
/// let c = create_memo(cx, move |_| {
|
||||
/// // this memo will *only* update when `a` changes
|
||||
/// a() + cx.untrack(move || b())
|
||||
/// a.get() + cx.untrack(move || b.get())
|
||||
/// });
|
||||
///
|
||||
/// assert_eq!(c(), 0);
|
||||
/// set_a(1);
|
||||
/// assert_eq!(c(), 1);
|
||||
/// set_b(1);
|
||||
/// assert_eq!(c.get(), 0);
|
||||
/// set_a.set(1);
|
||||
/// assert_eq!(c.get(), 1);
|
||||
/// set_b.set(1);
|
||||
/// // hasn't updated, because we untracked before reading b
|
||||
/// assert_eq!(c(), 1);
|
||||
/// set_a(2);
|
||||
/// assert_eq!(c(), 3);
|
||||
/// assert_eq!(c.get(), 1);
|
||||
/// set_a.set(2);
|
||||
/// assert_eq!(c.get(), 3);
|
||||
///
|
||||
/// # });
|
||||
/// ```
|
||||
@@ -209,37 +208,14 @@ impl Scope {
|
||||
)]
|
||||
#[inline(always)]
|
||||
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let untracked_result;
|
||||
|
||||
SpecialNonReactiveZone::enter();
|
||||
|
||||
let prev_observer =
|
||||
SetObserverOnDrop(self.runtime, runtime.observer.take());
|
||||
|
||||
untracked_result = f();
|
||||
|
||||
runtime.observer.set(prev_observer.1);
|
||||
std::mem::forget(prev_observer); // avoid Drop
|
||||
|
||||
SpecialNonReactiveZone::exit();
|
||||
|
||||
untracked_result
|
||||
})
|
||||
.expect(
|
||||
"tried to run untracked function in a runtime that has been \
|
||||
disposed",
|
||||
)
|
||||
self.runtime.untrack(f, false)
|
||||
}
|
||||
}
|
||||
|
||||
struct SetObserverOnDrop(RuntimeId, Option<NodeId>);
|
||||
|
||||
impl Drop for SetObserverOnDrop {
|
||||
fn drop(&mut self) {
|
||||
_ = with_runtime(self.0, |rt| {
|
||||
rt.observer.set(self.1);
|
||||
});
|
||||
#[doc(hidden)]
|
||||
/// Suspends reactive tracking but keeps the diagnostic warnings for
|
||||
/// untracked functions.
|
||||
pub fn untrack_with_diagnostics<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
self.runtime.untrack(f, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +325,27 @@ impl Scope {
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn remove_scope_property(&self, prop: ScopeProperty) {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let scopes = runtime.scopes.borrow();
|
||||
|
||||
if let Some(scope) = scopes.get(self.id) {
|
||||
let mut scope = scope.borrow_mut();
|
||||
if let Some(index) = scope.iter().position(|p| p == &prop) {
|
||||
scope.swap_remove(index);
|
||||
}
|
||||
} else {
|
||||
console_warn(
|
||||
"tried to remove property to a scope that has been \
|
||||
disposed",
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, features = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
/// Returns the the parent Scope, if any.
|
||||
pub fn parent(&self) -> Option<Scope> {
|
||||
match with_runtime(self.runtime, |runtime| {
|
||||
@@ -392,7 +389,7 @@ slotmap::new_key_type! {
|
||||
pub struct ScopeId;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub(crate) enum ScopeProperty {
|
||||
Trigger(NodeId),
|
||||
Signal(NodeId),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user