mirror of
https://github.com/leptos-rs/leptos.git
synced 2026-01-08 17:04:23 -05:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfd5c98f97 | ||
|
|
2e63bb1f50 | ||
|
|
982c8f6b5a | ||
|
|
12c4c115f3 | ||
|
|
d4d20ecdb0 | ||
|
|
b78919c6ed | ||
|
|
abb9320e31 | ||
|
|
875d2d5a3a | ||
|
|
42a58855a0 | ||
|
|
9d142758ec | ||
|
|
2faddd85cb | ||
|
|
ddd463748d | ||
|
|
71ee4cd09d | ||
|
|
08c56f7d6c | ||
|
|
e1ba26b62c | ||
|
|
309f0bf826 | ||
|
|
1698ffa7db | ||
|
|
556955cf1a | ||
|
|
a9f778459a | ||
|
|
f2ac412253 | ||
|
|
3bd52fcc9d | ||
|
|
b9bd1e103c | ||
|
|
55f9081465 | ||
|
|
0bac16dba0 | ||
|
|
a8a9c575b5 | ||
|
|
15ec855db5 | ||
|
|
b8f79a7e56 | ||
|
|
b988ee85f4 | ||
|
|
d6e166f105 | ||
|
|
53ceca8ff8 | ||
|
|
f2f9759138 | ||
|
|
817152ff39 | ||
|
|
38daaf3b72 | ||
|
|
666d53e2ba | ||
|
|
b55e9a9e64 |
5
.github/workflows/check-examples.yml
vendored
5
.github/workflows/check-examples.yml
vendored
@@ -11,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -39,9 +39,6 @@ jobs:
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
|
||||
45
.github/workflows/check-stable.yml
vendored
Normal file
45
.github/workflows/check-stable.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make check-stable
|
||||
5
.github/workflows/check.yml
vendored
5
.github/workflows/check.yml
vendored
@@ -11,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Run `cargo check` ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -39,9 +39,6 @@ jobs:
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all libraries
|
||||
|
||||
34
.github/workflows/fmt.yml
vendored
Normal file
34
.github/workflows/fmt.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run rustfmt
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -11,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -39,9 +39,6 @@ jobs:
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.2" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.2" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.2" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.2" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.2" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.2" }
|
||||
leptos_router = { path = "./router", version = "0.2.2" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.2" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.3" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.3" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.3" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.3" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.3" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.3" }
|
||||
leptos_router = { path = "./router", version = "0.2.3" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.3" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
# make tasks run at the workspace root
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["check", "check-examples", "test"]
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = ["check-all", "check-wasm"]
|
||||
dependencies = [
|
||||
"check-all",
|
||||
"check-wasm",
|
||||
"check-all-release",
|
||||
"check-wasm-release",
|
||||
]
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
@@ -23,14 +25,21 @@ install_crate = "cargo-all-features"
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm", path = "leptos" }]
|
||||
|
||||
[tasks.check-all-release]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-wasm-release]
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm-release", path = "leptos" }]
|
||||
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter" },
|
||||
{ name = "check", path = "examples/counter_isomorphic" },
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
{ name = "check", path = "examples/error_boundary" },
|
||||
{ name = "check", path = "examples/errors_axum" },
|
||||
{ name = "check", path = "examples/fetch" },
|
||||
@@ -43,12 +52,20 @@ dependencies = [
|
||||
{ name = "check", path = "examples/ssr_modes" },
|
||||
{ name = "check", path = "examples/ssr_modes_axum" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/tailwind_csr_trunk" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_viz" },
|
||||
{ name = "check", path = "examples/todomvc" },
|
||||
]
|
||||
|
||||
[tasks.check-stable]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
|
||||
@@ -78,7 +78,7 @@ rustup target add wasm32-unknown-unknown
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0", features = ["stable"] }`
|
||||
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)
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
@@ -27,4 +28,4 @@ features = [
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement"
|
||||
]
|
||||
]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
extern crate test;
|
||||
|
||||
//mod reactive;
|
||||
mod ssr;
|
||||
mod todomvc;
|
||||
mod reactive;
|
||||
//mod ssr;
|
||||
//mod todomvc;
|
||||
|
||||
@@ -1,35 +1,114 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use test::Bencher;
|
||||
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
fn leptos_deep_creation(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_deep_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_down(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
fn leptos_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(|cx| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
@@ -48,17 +127,20 @@ fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_scope, create_signal};
|
||||
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope({
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
@@ -76,16 +158,183 @@ fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
fn l021_deep_creation(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_narrowing_down(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
#[bench]
|
||||
fn l021_narrowing_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
writes[1].update(|n| *n += 1);
|
||||
writes[10].update(|n| *n += 1);
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for disposer in disposers {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_narrowing_down(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
@@ -97,13 +346,80 @@ fn sycamore_create_1000_signals(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
fn sycamore_fanning_out(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let sig = create_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 1000);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_creation(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(*memos[999].get(), 1001);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
#[bench]
|
||||
fn sycamore_narrowing_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
@@ -129,7 +445,7 @@ fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
fn sycamore_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
|
||||
@@ -60,7 +60,7 @@ Your directory structure should now look something like this
|
||||
leptos_tutorial
|
||||
├── src
|
||||
│ └── main.rs
|
||||
├── Cargo.html
|
||||
├── Cargo.toml
|
||||
├── index.html
|
||||
```
|
||||
|
||||
|
||||
@@ -21,10 +21,11 @@
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router]()
|
||||
- [Fundamentals]()
|
||||
- [defining `<Routes/>`]()
|
||||
- [`<A/>`]()
|
||||
- [Router](./router/README.md)
|
||||
- [Defining `<Routes/>`](./router/16_routes.md)
|
||||
- [Nested Routing](./router/17_nested_routing.md)
|
||||
- [Params and Queries](./router/18_params_and_queries.md)
|
||||
- [`<A/>`](./router/19_a.md)
|
||||
- [`<Form/>`]()
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Metadata]()
|
||||
@@ -33,6 +34,9 @@
|
||||
- [`cargo-leptos`]()
|
||||
- [Hydration Footguns]()
|
||||
- [Request/Response]()
|
||||
- [Extractors]()
|
||||
- [Axum]()
|
||||
- [Actix]()
|
||||
- [Headers]()
|
||||
- [Cookies]()
|
||||
- [Server Functions]()
|
||||
@@ -41,3 +45,4 @@
|
||||
- [`<ActionForm/>`s]()
|
||||
- [Turning off WebAssembly]()
|
||||
- [Advanced Reactivity]()
|
||||
- [Appendix: Optimizing WASM Binary Size]()
|
||||
|
||||
@@ -69,4 +69,4 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
|
||||
|
||||
This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
|
||||
101
docs/book/src/router/16_routes.md
Normal file
101
docs/book/src/router/16_routes.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Defining Routes
|
||||
|
||||
## Getting Started
|
||||
|
||||
It’s easy to get started with the router.
|
||||
|
||||
First things first, make sure you’ve added the `leptos_router` package to your dependencies.
|
||||
|
||||
> It’s important that the router is a separate package from `leptos` itself. This means that everything in the router can be defined in user-land code. If you want to create your own router, or use no router, you’re completely free to do that!
|
||||
|
||||
And import the relevant types from the router, either with something like
|
||||
|
||||
```rust
|
||||
use leptos_router::{Route, RouteProps, Router, RouterProps, Routes, RoutesProps};
|
||||
```
|
||||
|
||||
or simply
|
||||
|
||||
```rust
|
||||
use leptos_router::*;
|
||||
```
|
||||
|
||||
## Providing the `<Router/>`
|
||||
|
||||
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
|
||||
|
||||
> You shouldn’t try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
|
||||
|
||||
Let’s start with a simple `<App/>` component using the router:
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
</nav>
|
||||
<main>
|
||||
/* ... */
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Defining `<Routes/>`
|
||||
|
||||
The [`<Routes/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Route.html) component.
|
||||
|
||||
You should place the `<Routes/>` component at the location within your app where you want routes to be rendered. Everything outside `<Routes/>` will be present on every page, so you can leave things like a navigation bar or menu outside the `<Routes/>`.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
</nav>
|
||||
<main>
|
||||
// all our routes will appear inside <main>
|
||||
<Routes>
|
||||
/* ... */
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Individual routes are defined by providing children to `<Routes/>` with the `<Route/>` component. `<Route/>` takes a `path` and a `view`. When the current location matches `path`, the `view` will be created and displayed.
|
||||
|
||||
The `path` can include
|
||||
|
||||
- a static path (`/users`),
|
||||
- dynamic, named parameters beginning with a colon (`/:id`),
|
||||
- and/or a wildcard beginning with an asterisk (`/user/*any`)
|
||||
|
||||
The `view` is a function that takes a `Scope` and returns a view.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home/> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The router scores each route to see how good a match it is, so you can define your routes in any order.
|
||||
|
||||
Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` you’ll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
|
||||
|
||||
Simple enough?
|
||||
170
docs/book/src/router/17_nested_routing.md
Normal file
170
docs/book/src/router/17_nested_routing.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Nested Routing
|
||||
|
||||
We just defined the following set of routes:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
There’s a certain amount of duplication here: `/users` and `/users/:id`. This is fine for a small app, but you can probably already tell it won’t scale well. Wouldn’t it be nice if we could nest these routes?
|
||||
|
||||
Well... you can!
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Route>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
But wait. We’ve just subtly changed what our application does.
|
||||
|
||||
The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if there’s anything you don’t understand.
|
||||
|
||||
# Nested Routes as Layout
|
||||
|
||||
Nested routes are a form of layout, not a method of route definition.
|
||||
|
||||
Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple `<Route/>`s on the page at the same time, side by side.
|
||||
|
||||
Let’s look back at our practical example.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- If I go to `/users`, I get the `<Users/>` component.
|
||||
- If I go to `/users/3`, I get the `<UserProfile/>` component (with the parameter `id` set to `3`; more on that later)
|
||||
|
||||
Let’s say I use nested routes instead:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- If I go to `/users/3`, the path matches two `<Route/>`s: `<Users/>` and `<UserProfile/>`.
|
||||
- If I go to `/users`, the path is not matched.
|
||||
|
||||
I actually need to add a fallback route
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="" view=|cx| view! { cx, <NoUser /> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
Now:
|
||||
|
||||
- If I go to `/users/3`, the path matches `<Users/>` and `<UserProfile/>`.
|
||||
- If I go to `/users`, the path matches `<Users/>` and `<NoUser/>`.
|
||||
|
||||
When I use nested routes, in other words, each **path** can match multiple **routes**: each URL can render the views provided by multiple `<Route/>` components, at the same time, on the same page.
|
||||
|
||||
This may be counter-intuitive, but it’s very powerful, for reasons you’ll hopefully see in a few minutes.
|
||||
|
||||
## Why Nested Routing?
|
||||
|
||||
Why bother with this?
|
||||
|
||||
Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like `/contacts/greg`, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If there’s no contact selected, maybe you want to show a little instructional text.
|
||||
|
||||
You can easily define this with nested routes
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }/>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
You can go even deeper. Say you want to have tabs for each contact’s address, email/phone, and your conversations with them. You can add _another_ set of nested routes inside `:id`:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
|
||||
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
|
||||
<Route path="address" view=|cx| view! { cx, <Address/> }/>
|
||||
<Route path="messages" view=|cx| view! { cx, <Messages/> }/>
|
||||
</Route>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The main page of the [Remix website](https://remix.run/), a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice.
|
||||
|
||||
## `<Outlet/>`
|
||||
|
||||
Parent routes do not automatically render their nested routes. After all, they are just components; they don’t know exactly where they should render their children, and “just stick at at the end of the parent component” is not a great answer.
|
||||
|
||||
Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things:
|
||||
|
||||
- if there is no nested route that has been matched, it shows nothing
|
||||
- if there is a nested route that has been matched, it shows its `view`
|
||||
|
||||
That’s all! But it’s important to know and to remember, because it’s a common source of “Why isn’t this working?” frustration. If you don’t provide an `<Outlet/>`, the nested route won’t be displayed.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
let contacts = todo!();
|
||||
|
||||
view! { cx,
|
||||
<div style="display: flex">
|
||||
// the contact list
|
||||
<For each=contacts
|
||||
key=|contact| contact.id
|
||||
view=|cx, contact| todo!()
|
||||
>
|
||||
// the nested child, if any
|
||||
// don’t forget this!
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Nested Routing and Performance
|
||||
|
||||
All of this is nice, conceptually, but again—what’s the big deal?
|
||||
|
||||
Performance.
|
||||
|
||||
In a fine-grained reactive library like Leptos, it’s always important to do the least amount of rendering work you can. Because we’re working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy.
|
||||
|
||||
Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the `<ContactList/>` should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of `<ContactList/>`, navigating from Greg to Alice to Bob won’t clear the search.
|
||||
|
||||
In fact, in this case, we don’t even need to rerender the `<Contact/>` component when moving between contacts. The router will just reactively update the `:id` parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, we’ll update single text nodes to change the contact’s name, address, and so on, without doing _any_ additional rerendering.
|
||||
|
||||
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
77
docs/book/src/router/18_params_and_queries.md
Normal file
77
docs/book/src/router/18_params_and_queries.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Params and Queries
|
||||
|
||||
Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point.
|
||||
|
||||
There are two ways you can do this:
|
||||
|
||||
1. named route **params** like `id` in `/users/:id`
|
||||
2. named route **queries** like `q` in `/search?q=Foo`
|
||||
|
||||
Because of the way URLs are built, you can access the query from _any_ `<Route/>` view. You can access route params from the `<Route/>` that defines them or any of its nested children.
|
||||
|
||||
Accessing params and queries is pretty simple with a couple of hooks:
|
||||
|
||||
- [`use_query`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
|
||||
- [`use_params`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
|
||||
|
||||
Each of these comes with a typed option (`use_query` and `use_params`) and an untyped option (`use_query_map` and `use_params_map`).
|
||||
|
||||
The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.2.3/leptos_router/trait.Params.html) trait on a struct.
|
||||
|
||||
> `Params` is a very lightweight trait to convert a flat key-value map of strings into a struct by applying `FromStr` to each field. Because of the flat structure of route params and URL queries, it’s significantly less flexible than something like `serde`; it also adds much less weight to your binary.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[derive(Params)]
|
||||
struct ContactParams {
|
||||
id: usize
|
||||
}
|
||||
|
||||
#[derive(Params)]
|
||||
struct ContactSearch {
|
||||
q: String
|
||||
}
|
||||
```
|
||||
|
||||
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure you’re importing the right one for the derive macro.
|
||||
|
||||
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
|
||||
|
||||
The typed versions return `Memo<Result<T>, _>`. It’s a Memo so it reacts to changes in the URL. It’s a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
|
||||
|
||||
```rust
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let query = use_query::<ContactSearch>(cx);
|
||||
|
||||
// id: || -> usize
|
||||
let id = move || {
|
||||
params.with(|params| {
|
||||
params
|
||||
.map(|params| params.id)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
|
||||
|
||||
```rust
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let query = use_query::<ContactSearch>(cx);
|
||||
|
||||
// id: || -> Option<String>
|
||||
let id = move || {
|
||||
params.with(|params| params.get("id").cloned())
|
||||
};
|
||||
```
|
||||
|
||||
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But it’s worth doing this for two reasons:
|
||||
|
||||
1. It’s correct, i.e., it forces you to consider the cases, “What if the user doesn’t pass a value for this query field? What if they pass an invalid value?”
|
||||
2. It’s performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replacing or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
|
||||
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explain them all yet.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
21
docs/book/src/router/19_a.md
Normal file
21
docs/book/src/router/19_a.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# The `<A/>` Component
|
||||
|
||||
Client-side navigation works perfectly fine with ordinary HTML `<a>` elements. The router adds a listener that handles every click on a `<a>` element and tries to handle it on the client side, i.e., without doing another round trip to the server to request HTML. This is what enables the snappy “single-page app” navigations you’re probably familiar with from most modern web apps.
|
||||
|
||||
The router will bail out of handling an `<a>` click under a number of situations
|
||||
|
||||
- the click event has had `prevent_default()` called on it
|
||||
- the <kbd>Meta</kbd>, <kbd>Alt</kbd>, <kbd>Ctrl</kbd>, or <kbd>Shift</kbd> keys were held during click
|
||||
- the `<a>` has a `target` or `download` attribute, or `rel="external"`
|
||||
- the link has a different origin from the current location
|
||||
|
||||
In other words, the router will only try to do a client-side navigation when it’s pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
|
||||
|
||||
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
|
||||
|
||||
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
|
||||
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., it’s a link to the page you’re on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if it’s a link to the page you’re currently on, you can match this attribute with a CSS selector.
|
||||
|
||||
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
23
docs/book/src/router/README.md
Normal file
23
docs/book/src/router/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Routing
|
||||
|
||||
## The Basics
|
||||
|
||||
Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?”
|
||||
|
||||
A URL consists of many parts. For example, the URL `https://leptos.dev/blog/search?q=Search#results` consists of
|
||||
|
||||
- a _scheme_: `https`
|
||||
- a _domain_: `leptos.dev`
|
||||
- a **path**: `/blog/search`
|
||||
- a **query** (or **search**): `?q=Search`
|
||||
- a _hash_: `#results`
|
||||
|
||||
The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page?
|
||||
|
||||
## The Philosophy
|
||||
|
||||
In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most appliations, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
|
||||
|
||||
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
|
||||
|
||||
The router handles most of this work for you by mapping the current location to particular components.
|
||||
@@ -30,7 +30,7 @@ Now let’s say I’d like to update the list of CSS classes on this element dyn
|
||||
For example, let’s say I want to add the class `red` when the count is odd. I can
|
||||
do this using the `class:` syntax.
|
||||
```rust
|
||||
class:red=move || count() & 1 == 1
|
||||
class:red=move || count() % 2 == 1
|
||||
```
|
||||
`class:` attributes take
|
||||
1. the class name, following the colon (`red`)
|
||||
|
||||
@@ -238,7 +238,7 @@ which allows you to easily pass props with different values.
|
||||
|
||||
In this case, it’s helpful to know about the
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||
is a enumerated type that represents any kind of readable reactive signal. It can
|
||||
is an enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
reactive value.
|
||||
|
||||
@@ -12,7 +12,6 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
@@ -23,9 +22,9 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0.0"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -10,7 +10,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
args = ["+stable", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
args = ["+stable", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -2,7 +2,6 @@ use counters::{Counters, CountersProps};
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
|
||||
@@ -7,10 +7,8 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
@@ -18,20 +16,15 @@ leptos = { path = "../../../leptos/leptos", default-features = false, features =
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -13,7 +13,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use fetch::fetch_example;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(fetch_example)
|
||||
|
||||
@@ -7,12 +7,10 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
@@ -21,15 +19,13 @@ leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tracing = "0.1"
|
||||
# openssl = { version = "0.10", features = ["v110"] }
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
tracing = "0.1"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
|
||||
@@ -7,10 +7,8 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.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",
|
||||
@@ -21,7 +19,7 @@ leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
@@ -31,7 +29,6 @@ tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2"
|
||||
tracing = "0.1"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
|
||||
@@ -46,7 +46,6 @@ if #[cfg(feature = "ssr")] {
|
||||
use hackernews_axum::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
|
||||
@@ -127,6 +127,10 @@ pub fn Contact(cx: Scope) -> impl IntoView {
|
||||
get_contact,
|
||||
);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
log!("params = {:#?}", params.get());
|
||||
});
|
||||
|
||||
let contact_display = move || match contact.read(cx) {
|
||||
// None => loading, but will be caught by Suspense fallback
|
||||
// I'm only doing this explicitly for the example
|
||||
|
||||
@@ -2,7 +2,6 @@ use leptos::*;
|
||||
use router::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <RouterExample/> })
|
||||
|
||||
@@ -13,19 +13,15 @@ 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 = { version = "0.2.0", default-features = false, features = [
|
||||
"serde",
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { version = "0.2.0", default-features = false }
|
||||
leptos_axum = { version = "0.2.0", optional = true }
|
||||
leptos_router = { version = "0.2.0", default-features = false }
|
||||
leptos_reactive = { version = "0.2.0", default-features = false }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
@@ -36,12 +32,15 @@ sqlx = { version = "0.6.2", features = [
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_sessions_auth = { version = "7.0.0", features = [ "sqlite-rustls" ], optional = true }
|
||||
axum_database_sessions = { version = "7.0.0", features = [ "sqlite-rustls" ], optional = true }
|
||||
axum_sessions_auth = { version = "7.0.0", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_database_sessions = { version = "7.0.0", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
bcrypt = { version = "0.14", optional = true }
|
||||
async-trait = {version = "0.1.64", optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
|
||||
@@ -15,7 +15,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ leptos_actix = { path = "../../integrations/actix", default-features = false, op
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@@ -19,12 +19,11 @@ leptos_axum = { path = "../../integrations/axum", default-features = false, opti
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4"
|
||||
thiserror = "1"
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["time"], optional = true}
|
||||
tokio = { version = "1", features = ["time"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -9,14 +9,14 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("hydrate mode - hydrating");
|
||||
log!("hydrate mode - hydrating");
|
||||
|
||||
leptos::mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
leptos::mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
else if #[cfg(feature = "csr")] {
|
||||
@@ -35,5 +35,5 @@ cfg_if! {
|
||||
view! { cx, <App /> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
examples/tailwind_csr_trunk/.gitignore
vendored
Normal file
10
examples/tailwind_csr_trunk/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
21
examples/tailwind_csr_trunk/Cargo.toml
Normal file
21
examples/tailwind_csr_trunk/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "tailwind-csr-trunk"
|
||||
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"] }
|
||||
log = "0.4"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
console_log = { version = "0.2"}
|
||||
console_error_panic_hook = { version = "0.1"}
|
||||
|
||||
21
examples/tailwind_csr_trunk/LICENSE
Normal file
21
examples/tailwind_csr_trunk/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 henrik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
9
examples/tailwind_csr_trunk/Makefile.toml
Normal file
9
examples/tailwind_csr_trunk/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
70
examples/tailwind_csr_trunk/README.md
Normal file
70
examples/tailwind_csr_trunk/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Leptos Starter Template
|
||||
|
||||
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [trunk](https://github.com/thedodd/trunk) tool.
|
||||
|
||||
|
||||
Install Tailwind and build the CSS:
|
||||
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
|
||||
|
||||
Install trunk to client side render this bundle.
|
||||
|
||||
`cargo install trunk`
|
||||
Then the site can be served with `trunk serve --open`
|
||||
|
||||
The browser will automatically open [http://127.0.0.1:8080//](http://127.0.0.1:8080//)
|
||||
|
||||
You can begin editing your app at `src/app.rs`.
|
||||
|
||||
## Installing Tailwind
|
||||
|
||||
You can install Tailwind using `npm`:
|
||||
|
||||
```bash
|
||||
npm install -D tailwindcss
|
||||
```
|
||||
|
||||
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
|
||||
|
||||
## Setting up with VS Code and Additional Tools
|
||||
|
||||
If you're using VS Code, add the following to your `settings.json`
|
||||
|
||||
```json
|
||||
"emmet.includeLanguages": {
|
||||
"rust": "html",
|
||||
"*.rs": "html"
|
||||
},
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"rust": "html",
|
||||
"*.rs": "html"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.rs": "rust"
|
||||
},
|
||||
"editor.quickSuggestions": {
|
||||
"other": "on",
|
||||
"comments": "on",
|
||||
"strings": true
|
||||
},
|
||||
"css.validate": false,
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Notes about Tooling
|
||||
|
||||
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
|
||||
|
||||
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
|
||||
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
|
||||
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
|
||||
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
|
||||
5. `npm install -g sass` - install `dart-sass` (should be optional in future
|
||||
|
||||
|
||||
## Attribution
|
||||
This is based on the original Tailwind example (../examples/tailwind)
|
||||
74
examples/tailwind_csr_trunk/end2end/package-lock.json
generated
Normal file
74
examples/tailwind_csr_trunk/end2end/package-lock.json
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/tailwind_csr_trunk/end2end/package.json
Normal file
13
examples/tailwind_csr_trunk/end2end/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
}
|
||||
107
examples/tailwind_csr_trunk/end2end/playwright.config.ts
Normal file
107
examples/tailwind_csr_trunk/end2end/playwright.config.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./tests",
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* 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 ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* 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: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
await page.goto("http://localhost:8080/");
|
||||
|
||||
await expect(page).toHaveTitle("Leptos • Counter with Tailwind");
|
||||
|
||||
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
|
||||
});
|
||||
14
examples/tailwind_csr_trunk/index.html
Normal file
14
examples/tailwind_csr_trunk/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico" />
|
||||
<link data-trunk rel="css" href="/style/output.css" />
|
||||
<title>Leptos • Counter with Tailwind</title>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
|
||||
</html>
|
||||
3
examples/tailwind_csr_trunk/input.css
Normal file
3
examples/tailwind_csr_trunk/input.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
BIN
examples/tailwind_csr_trunk/public/favicon.ico
Normal file
BIN
examples/tailwind_csr_trunk/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
43
examples/tailwind_csr_trunk/src/app.rs
Normal file
43
examples/tailwind_csr_trunk/src/app.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<Stylesheet id="leptos" href="/pkg/tailwind.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view= move |cx| view! { cx, <Home/> }/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<div class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
"Something's here | "
|
||||
{move || if count() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count().to_string()
|
||||
}}
|
||||
" | Some more text"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
15
examples/tailwind_csr_trunk/src/main.rs
Normal file
15
examples/tailwind_csr_trunk/src/main.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
mod app;
|
||||
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("csr mode - mounting to body");
|
||||
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <App /> }
|
||||
});
|
||||
}
|
||||
583
examples/tailwind_csr_trunk/style/output.css
Normal file
583
examples/tailwind_csr_trunk/style/output.css
Normal file
@@ -0,0 +1,583 @@
|
||||
/*
|
||||
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.my-0 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.max-w-3xl {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.bg-amber-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.px-10 {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.px-5 {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.py-3 {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.pb-10 {
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-sky-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
|
||||
}
|
||||
10
examples/tailwind_csr_trunk/tailwind.config.js
Normal file
10
examples/tailwind_csr_trunk/tailwind.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -4,6 +4,15 @@ args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = ["check-debug", "check-release"]
|
||||
|
||||
[tasks.check-debug]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-release]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -7,7 +7,6 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
@@ -18,13 +17,9 @@ leptos = { path = "../../leptos", default-features = false, features = [
|
||||
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_reactive = { path = "../../leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
@@ -35,7 +30,6 @@ sqlx = { version = "0.6.2", features = [
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -13,7 +13,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -7,13 +7,12 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.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",
|
||||
"serde",
|
||||
] }
|
||||
leptos_viz = { path = "../../integrations/viz", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
@@ -21,19 +20,15 @@ leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
viz = { version = "0.4.8", features = ["serve"], optional = true }
|
||||
tokio = { version = "1.25.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
@@ -47,7 +42,7 @@ ssr = [
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_viz"
|
||||
"dep:leptos_viz",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -13,7 +13,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use leptos::*;
|
||||
pub use todomvc::*;
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <TodoMVC/> })
|
||||
|
||||
@@ -15,7 +15,6 @@ hyper = "0.14.23"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
@@ -12,5 +12,4 @@ futures = "0.3"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_hot_reload = { workspace = true }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
|
||||
@@ -15,7 +15,6 @@ hyper = "0.14.23"
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
clear = true
|
||||
dependencies = ["check-hydrate", "check-csr"]
|
||||
|
||||
[tasks.check-wasm-release]
|
||||
clear = true
|
||||
dependencies = ["check-hydrate-release", "check-csr-release"]
|
||||
|
||||
[tasks.check-hydrate]
|
||||
command = "cargo"
|
||||
args = [
|
||||
@@ -19,3 +23,23 @@ args = [
|
||||
"--features=csr",
|
||||
"--target=wasm32-unknown-unknown",
|
||||
]
|
||||
|
||||
[tasks.check-hydrate-release]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"check",
|
||||
"--release",
|
||||
"--no-default-features",
|
||||
"--features=hydrate",
|
||||
"--target=wasm32-unknown-unknown",
|
||||
]
|
||||
|
||||
[tasks.check-csr-release]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"check",
|
||||
"--release",
|
||||
"--no-default-features",
|
||||
"--features=csr",
|
||||
"--target=wasm32-unknown-unknown",
|
||||
]
|
||||
|
||||
@@ -150,12 +150,15 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
|
||||
pub mod ssr {
|
||||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, debug_warn, document, error, ev,
|
||||
helpers::{
|
||||
event_target, event_target_checked, event_target_value,
|
||||
request_animation_frame, request_idle_callback, set_interval,
|
||||
set_timeout, window_event_listener,
|
||||
request_animation_frame, request_animation_frame_with_handle,
|
||||
request_idle_callback, request_idle_callback_with_handle, set_interval,
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener,
|
||||
},
|
||||
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
|
||||
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
|
||||
|
||||
@@ -116,9 +116,12 @@ where
|
||||
let suspense_context = use_context::<SuspenseContext>(cx)
|
||||
.expect("there to be a SuspenseContext");
|
||||
|
||||
if is_first_run(&first_run, &suspense_context) {
|
||||
let has_local_only = suspense_context.has_local_only();
|
||||
if cfg!(feature = "hydrate") || !first_run.get() {
|
||||
*prev_children.borrow_mut() = Some(frag.nodes.clone());
|
||||
}
|
||||
if is_first_run(&first_run, &suspense_context) {
|
||||
let has_local_only = suspense_context.has_local_only()
|
||||
|| cfg!(feature = "csr");
|
||||
if !has_local_only || child_runs.get() > 0 {
|
||||
first_run.set(false);
|
||||
}
|
||||
@@ -138,17 +141,21 @@ fn is_first_run(
|
||||
first_run: &Rc<Cell<bool>>,
|
||||
suspense_context: &SuspenseContext,
|
||||
) -> bool {
|
||||
match (
|
||||
first_run.get(),
|
||||
cfg!(feature = "hydrate"),
|
||||
suspense_context.has_local_only(),
|
||||
) {
|
||||
(false, _, _) => false,
|
||||
// is in hydrate mode, and has non-local resources (so, has streamed)
|
||||
(_, false, false) => false,
|
||||
// is in hydrate mode, but with only local resources (so, has not streamed)
|
||||
(_, false, true) => true,
|
||||
// either SSR or client mode: it's the first run
|
||||
(_, true, _) => true,
|
||||
if cfg!(feature = "csr") {
|
||||
false
|
||||
} else {
|
||||
match (
|
||||
first_run.get(),
|
||||
cfg!(feature = "hydrate"),
|
||||
suspense_context.has_local_only(),
|
||||
) {
|
||||
(false, _, _) => false,
|
||||
// SSR and has non-local resources (so, has streamed)
|
||||
(_, false, false) => false,
|
||||
// SSR but with only local resources (so, has not streamed)
|
||||
(_, false, true) => true,
|
||||
// hydrate: it's the first run
|
||||
(_, true, _) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,12 @@ crate-type = ["cdylib", "rlib"]
|
||||
leptos = { path = "../../../leptos", default-features = false }
|
||||
actix-web = { version = "4", optional = true }
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true}
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
gloo = { version = "0.8", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
gloo-timers = { version = "0.2", features = ["futures"] }
|
||||
futures = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
ssr = ["leptos/ssr", "dep:actix-files", "dep:actix-web"]
|
||||
hydrate = ["leptos/hydrate", "dep:wasm-bindgen", "dep:gloo"]
|
||||
hydrate = ["leptos/hydrate", "dep:wasm-bindgen", "dep:gloo"]
|
||||
|
||||
@@ -9,7 +9,5 @@ gloo = { version = "0.8", features = ["futures"] }
|
||||
leptos = { path = "../../../leptos", features = ["tracing"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = "0.3"
|
||||
|
||||
[workspace]
|
||||
|
||||
@@ -207,7 +207,10 @@ where
|
||||
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child.get_closing_node().next_sibling().as_ref()
|
||||
&& child
|
||||
.get_closing_node()
|
||||
.next_non_view_marker_sibling()
|
||||
.as_ref()
|
||||
!= Some(&closing);
|
||||
|
||||
// If the previous child was a text node, we would like to
|
||||
@@ -241,7 +244,7 @@ where
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_sibling()
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
@@ -300,7 +303,7 @@ where
|
||||
&& new_child.get_text().is_some()
|
||||
{
|
||||
let t = closing
|
||||
.previous_sibling()
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>();
|
||||
|
||||
@@ -364,3 +367,49 @@ where
|
||||
View::CoreComponent(crate::CoreComponent::DynChild(component))
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use web_sys::Node;
|
||||
|
||||
trait NonViewMarkerSibling {
|
||||
fn next_non_view_marker_sibling(&self) -> Option<Node>;
|
||||
|
||||
fn previous_non_view_marker_sibling(&self) -> Option<Node>;
|
||||
}
|
||||
|
||||
impl NonViewMarkerSibling for web_sys::Node {
|
||||
fn next_non_view_marker_sibling(&self) -> Option<Node> {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
self.next_sibling().and_then(|node| {
|
||||
if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") {
|
||||
node.next_sibling()
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.next_sibling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_non_view_marker_sibling(&self) -> Option<Node> {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
self.previous_sibling().and_then(|node| {
|
||||
if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") {
|
||||
node.previous_sibling()
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.previous_sibling()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! A variety of DOM utility functions.
|
||||
|
||||
use crate::{is_server, window};
|
||||
use leptos_reactive::{on_cleanup, Scope};
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
|
||||
|
||||
@@ -80,10 +81,33 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
|
||||
.checked()
|
||||
}
|
||||
|
||||
/// Runs the given function between the next repaint
|
||||
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
|
||||
/// Handle that is generated by [request_animation_frame_with_handle] and can
|
||||
/// be used to cancel the animation frame request.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct AnimationFrameRequestHandle(i32);
|
||||
|
||||
impl AnimationFrameRequestHandle {
|
||||
/// Cancels the animation frame request to which this refers.
|
||||
/// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame)
|
||||
pub fn cancel(&self) {
|
||||
_ = window().cancel_animation_frame(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the given function between the next repaint using
|
||||
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
_ = request_animation_frame_with_handle(cb);
|
||||
}
|
||||
|
||||
/// Runs the given function between the next repaint using
|
||||
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
|
||||
/// returning a cancelable handle.
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_animation_frame_with_handle(
|
||||
cb: impl FnOnce() + 'static,
|
||||
) -> Result<AnimationFrameRequestHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
@@ -95,13 +119,38 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(cb);
|
||||
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
|
||||
window()
|
||||
.request_animation_frame(cb.as_ref().unchecked_ref())
|
||||
.map(AnimationFrameRequestHandle)
|
||||
}
|
||||
|
||||
/// Queues the given function during an idle period
|
||||
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
|
||||
/// Handle that is generated by [request_idle_callback_with_handle] and can be
|
||||
/// used to cancel the idle callback.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct IdleCallbackHandle(u32);
|
||||
|
||||
impl IdleCallbackHandle {
|
||||
/// Cancels the idle callback to which this refers.
|
||||
/// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelIdleCallback)
|
||||
pub fn cancel(&self) {
|
||||
window().cancel_idle_callback(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues the given function during an idle period using
|
||||
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
_ = request_idle_callback_with_handle(cb);
|
||||
}
|
||||
|
||||
/// Queues the given function during an idle period using
|
||||
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
|
||||
/// returning a cancelable handle.
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_idle_callback_with_handle(
|
||||
cb: impl Fn() + 'static,
|
||||
) -> Result<IdleCallbackHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
@@ -113,7 +162,21 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
|
||||
window()
|
||||
.request_idle_callback(cb.as_ref().unchecked_ref())
|
||||
.map(IdleCallbackHandle)
|
||||
}
|
||||
|
||||
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct TimeoutHandle(i32);
|
||||
|
||||
impl TimeoutHandle {
|
||||
/// Cancels the timeout to which this refers.
|
||||
/// See [`clearTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout)
|
||||
pub fn clear(&self) {
|
||||
window().clear_timeout_with_handle(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the given function after the given duration of time has passed.
|
||||
@@ -123,6 +186,19 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
_ = set_timeout_with_handle(cb, duration);
|
||||
}
|
||||
|
||||
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
|
||||
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_timeout_with_handle(
|
||||
cb: impl FnOnce() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<TimeoutHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
@@ -134,10 +210,83 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
|
||||
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
);
|
||||
window()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)
|
||||
.map(TimeoutHandle)
|
||||
}
|
||||
|
||||
/// "Debounce" a callback function. This will cause it to wait for a period of `delay`
|
||||
/// after it is called. If it is called again during that period, it will wait
|
||||
/// `delay` before running, and so on. This can be used, for example, to wrap event
|
||||
/// listeners to prevent them from firing constantly as you type.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::{leptos_dom::helpers::debounce, *};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn DebouncedButton(cx: Scope) -> impl IntoView {
|
||||
/// let delay = std::time::Duration::from_millis(250);
|
||||
/// let on_click = debounce(cx, delay, move |_| {
|
||||
/// log!("...so many clicks!");
|
||||
/// });
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn debounce<T: 'static>(
|
||||
cx: Scope,
|
||||
delay: Duration,
|
||||
mut cb: impl FnMut(T) + 'static,
|
||||
) -> impl FnMut(T) {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |value| {
|
||||
let _guard = span.enter();
|
||||
cb(value);
|
||||
};
|
||||
}
|
||||
}
|
||||
let cb = Rc::new(RefCell::new(cb));
|
||||
|
||||
let timer = Rc::new(Cell::new(None::<TimeoutHandle>));
|
||||
|
||||
on_cleanup(cx, {
|
||||
let timer = Rc::clone(&timer);
|
||||
move || {
|
||||
if let Some(timer) = timer.take() {
|
||||
timer.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
move |arg| {
|
||||
if let Some(timer) = timer.take() {
|
||||
timer.clear();
|
||||
}
|
||||
let handle = set_timeout_with_handle(
|
||||
{
|
||||
let cb = Rc::clone(&cb);
|
||||
move || {
|
||||
cb.borrow_mut()(arg);
|
||||
}
|
||||
},
|
||||
delay,
|
||||
);
|
||||
if let Ok(handle) = handle {
|
||||
timer.set(Some(handle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle that is generated by [set_interval] and can be used to clear the interval.
|
||||
@@ -152,12 +301,16 @@ impl IntervalHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls.
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls,
|
||||
/// returning a cancelable handle.
|
||||
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
#[deprecated = "use set_interval_with_handle() instead. In the future, \
|
||||
set_interval() will no longer return a handle, for consistency \
|
||||
with other timer helper functions."]
|
||||
pub fn set_interval(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
@@ -181,6 +334,36 @@ pub fn set_interval(
|
||||
Ok(IntervalHandle(handle))
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls,
|
||||
/// returning a cancelable handle.
|
||||
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_interval_with_handle(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)?;
|
||||
Ok(IntervalHandle(handle))
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
|
||||
@@ -8,6 +8,7 @@ cfg_if! {
|
||||
use crate::macro_helpers::*;
|
||||
use crate::{mount_child, MountKind};
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
use std::cell::Cell;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// Trait alias for the trait bounts on [`ElementDescriptor`].
|
||||
@@ -20,6 +21,22 @@ cfg_if! {
|
||||
El: fmt::Debug + AsRef<web_sys::HtmlElement> + Clone
|
||||
{
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static IS_META: Cell<bool> = Cell::new(false);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn as_meta_tag<T>(f: impl FnOnce() -> T) -> T {
|
||||
IS_META.with(|m| m.set(true));
|
||||
let v = f();
|
||||
IS_META.with(|m| m.set(false));
|
||||
v
|
||||
}
|
||||
|
||||
fn is_meta_tag() -> bool {
|
||||
IS_META.with(|m| m.get())
|
||||
}
|
||||
} else {
|
||||
use crate::hydration::HydrationKey;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -35,6 +52,11 @@ cfg_if! {
|
||||
pub trait ElementDescriptorBounds: fmt::Debug {}
|
||||
|
||||
impl<El> ElementDescriptorBounds for El where El: fmt::Debug {}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn as_meta_tag<T>(f: impl FnOnce() -> T) -> T {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,9 +227,12 @@ impl Custom {
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
if !is_meta_tag() {
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for \
|
||||
hydration"
|
||||
);
|
||||
}
|
||||
|
||||
crate::document().create_element(&name).unwrap()
|
||||
}
|
||||
@@ -272,16 +297,40 @@ cfg_if! {
|
||||
pub struct HtmlElement<El: ElementDescriptor> {
|
||||
pub(crate) cx: Scope,
|
||||
pub(crate) element: El,
|
||||
#[educe(Debug(ignore))]
|
||||
pub(crate) attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
|
||||
#[educe(Debug(ignore))]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) children: SmallVec<[View; 4]>,
|
||||
#[educe(Debug(ignore))]
|
||||
pub(crate) prerendered: Option<Cow<'static, str>>,
|
||||
pub(crate) children: ElementChildren,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Clone, educe::Educe, PartialEq, Eq)]
|
||||
#[educe(Default)]
|
||||
pub(crate) enum ElementChildren {
|
||||
#[educe(Default)]
|
||||
Empty,
|
||||
Children(Vec<View>),
|
||||
InnerHtml(Cow<'static, str>),
|
||||
Chunks(Vec<StringOrView>)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone)]
|
||||
pub enum StringOrView {
|
||||
String(Cow<'static, str>),
|
||||
View(std::rc::Rc<dyn Fn() -> View>)
|
||||
}
|
||||
|
||||
impl PartialEq for StringOrView {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(StringOrView::String(a), StringOrView::String(b)) => a == b,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for StringOrView {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,9 +365,8 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
Self {
|
||||
cx,
|
||||
attrs: smallvec![],
|
||||
children: smallvec![],
|
||||
children: Default::default(),
|
||||
element,
|
||||
prerendered: None,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
}
|
||||
@@ -328,6 +376,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[deprecated = "Use HtmlElement::from_chunks() instead."]
|
||||
pub fn from_html(
|
||||
cx: Scope,
|
||||
element: El,
|
||||
@@ -336,9 +385,27 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
Self {
|
||||
cx,
|
||||
attrs: smallvec![],
|
||||
children: smallvec![],
|
||||
children: ElementChildren::Chunks(vec![StringOrView::String(
|
||||
html.into(),
|
||||
)]),
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn from_chunks(
|
||||
cx: Scope,
|
||||
element: El,
|
||||
chunks: impl IntoIterator<Item = StringOrView>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cx,
|
||||
attrs: smallvec![],
|
||||
children: ElementChildren::Chunks(chunks.into_iter().collect()),
|
||||
element,
|
||||
prerendered: Some(html.into()),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
@@ -382,7 +449,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
attrs,
|
||||
children,
|
||||
element,
|
||||
prerendered,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker
|
||||
} = self;
|
||||
@@ -391,7 +457,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
cx,
|
||||
attrs,
|
||||
children,
|
||||
prerendered,
|
||||
element: AnyElement {
|
||||
name: element.name(),
|
||||
is_void: element.is_void(),
|
||||
@@ -711,8 +776,23 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let mut this = self;
|
||||
let children = &mut this.children;
|
||||
|
||||
this.children.push(child);
|
||||
match children {
|
||||
ElementChildren::Empty => {
|
||||
*children = ElementChildren::Children(vec![child]);
|
||||
}
|
||||
ElementChildren::Children(ref mut children) => {
|
||||
children.push(child);
|
||||
}
|
||||
_ => {
|
||||
crate::debug_warn!(
|
||||
"Don’t call .child() on an HtmlElement if you’ve \
|
||||
already called .inner_html() or \
|
||||
HtmlElement::from_chunks()."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
@@ -739,16 +819,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
{
|
||||
let mut this = self;
|
||||
|
||||
let child = HtmlElement::from_html(
|
||||
this.cx,
|
||||
Custom {
|
||||
name: "inner-html".into(),
|
||||
id: Default::default(),
|
||||
},
|
||||
html,
|
||||
);
|
||||
|
||||
this.children = smallvec![child.into_view(this.cx)];
|
||||
this.children = ElementChildren::InnerHtml(html);
|
||||
|
||||
this
|
||||
}
|
||||
@@ -768,7 +839,6 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
element,
|
||||
mut attrs,
|
||||
children,
|
||||
prerendered,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
..
|
||||
@@ -786,8 +856,7 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
}
|
||||
|
||||
element.attrs = attrs;
|
||||
element.children.extend(children);
|
||||
element.prerendered = prerendered;
|
||||
element.children = children;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -993,9 +1062,11 @@ fn create_leptos_element(
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
if !is_meta_tag() {
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
}
|
||||
|
||||
clone_element()
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ pub use html::HtmlElement;
|
||||
use html::{AnyElement, ElementDescriptor};
|
||||
pub use hydration::{HydrationCtx, HydrationKey};
|
||||
use leptos_reactive::Scope;
|
||||
#[cfg(feature = "stable")]
|
||||
use leptos_reactive::{
|
||||
MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
pub use logging::*;
|
||||
pub use macro_helpers::*;
|
||||
pub use node_ref::*;
|
||||
@@ -138,6 +142,72 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stable")]
|
||||
impl<T> IntoView for ReadSignal<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "ReadSignal<T>", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stable")]
|
||||
impl<T> IntoView for RwSignal<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "RwSignal<T>", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stable")]
|
||||
impl<T> IntoView for Memo<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "Memo<T>", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stable")]
|
||||
impl<T> IntoView for Signal<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "Signal<T>", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stable")]
|
||||
impl<T> IntoView for MaybeSignal<T>
|
||||
where
|
||||
T: IntoView + Clone,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "MaybeSignal<T>", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
DynChild::new(move || self.get()).into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
/// HTML element.
|
||||
@@ -161,14 +231,15 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
use crate::html::ElementChildren;
|
||||
|
||||
/// HTML element.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Element {
|
||||
name: Cow<'static, str>,
|
||||
is_void: bool,
|
||||
attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
|
||||
children: Vec<View>,
|
||||
prerendered: Option<Cow<'static, str>>,
|
||||
children: ElementChildren,
|
||||
id: HydrationKey,
|
||||
#[cfg(debug_assertions)]
|
||||
/// Optional marker for the view macro source, in debug mode.
|
||||
@@ -189,8 +260,10 @@ cfg_if! {
|
||||
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
|
||||
for child in &self.children {
|
||||
writeln!(pad_adapter, "{child:#?}")?;
|
||||
if let ElementChildren::Children(children) = &self.children {
|
||||
for child in children {
|
||||
writeln!(pad_adapter, "{child:#?}")?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(f, "</{}>", self.name)
|
||||
@@ -239,7 +312,6 @@ impl Element {
|
||||
attrs,
|
||||
children,
|
||||
id,
|
||||
prerendered,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
} = self;
|
||||
@@ -250,8 +322,7 @@ impl Element {
|
||||
cx,
|
||||
element,
|
||||
attrs,
|
||||
children: children.into_iter().collect(),
|
||||
prerendered,
|
||||
children,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker,
|
||||
}
|
||||
@@ -286,7 +357,6 @@ impl Element {
|
||||
attrs: Default::default(),
|
||||
children: Default::default(),
|
||||
id: el.hydration_id().clone(),
|
||||
prerendered: None,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
//! Server-side HTML rendering utilities.
|
||||
|
||||
use crate::{CoreComponent, HydrationCtx, IntoView, View};
|
||||
use crate::{
|
||||
html::{ElementChildren, StringOrView},
|
||||
CoreComponent, HydrationCtx, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
@@ -385,8 +388,19 @@ impl View {
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
let el_html = if let Some(prerendered) = el.prerendered {
|
||||
prerendered
|
||||
let el_html = if let ElementChildren::Chunks(chunks) =
|
||||
el.children
|
||||
{
|
||||
chunks
|
||||
.into_iter()
|
||||
.map(|chunk| match chunk {
|
||||
StringOrView::String(string) => string,
|
||||
StringOrView::View(view) => {
|
||||
view().render_to_string_helper()
|
||||
}
|
||||
})
|
||||
.join("")
|
||||
.into()
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
|
||||
@@ -421,11 +435,17 @@ impl View {
|
||||
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
|
||||
.into()
|
||||
} else {
|
||||
let children = el
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("");
|
||||
let children = match el.children {
|
||||
ElementChildren::Empty => "".into(),
|
||||
ElementChildren::Children(c) => c
|
||||
.into_iter()
|
||||
.map(View::render_to_string_helper)
|
||||
.join("")
|
||||
.into(),
|
||||
ElementChildren::InnerHtml(h) => h,
|
||||
// already handled this case above
|
||||
ElementChildren::Chunks(_) => unreachable!(),
|
||||
};
|
||||
|
||||
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
|
||||
.into()
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
//! Server-side HTML rendering utilities for in-order streaming and async rendering.
|
||||
|
||||
use crate::{ssr::render_serializers, CoreComponent, HydrationCtx, View};
|
||||
use crate::{
|
||||
html::{ElementChildren, StringOrView},
|
||||
ssr::render_serializers,
|
||||
CoreComponent, HydrationCtx, View,
|
||||
};
|
||||
use async_recursion::async_recursion;
|
||||
use cfg_if::cfg_if;
|
||||
use futures::{channel::mpsc::Sender, Stream, StreamExt};
|
||||
use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use leptos_reactive::{
|
||||
create_runtime, run_scope_undisposed, suspense::StreamChunk, RuntimeId,
|
||||
@@ -93,7 +97,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
)
|
||||
});
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::channel(1);
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
leptos_reactive::spawn_local(async move {
|
||||
handle_chunks(tx, chunks).await;
|
||||
});
|
||||
@@ -122,14 +126,15 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(mut tx: Sender<String>, chunks: Vec<StreamChunk>) {
|
||||
async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
|
||||
let mut buffer = String::new();
|
||||
for chunk in chunks {
|
||||
match chunk {
|
||||
StreamChunk::Sync(sync) => buffer.push_str(&sync),
|
||||
StreamChunk::Async(suspended) => {
|
||||
// add static HTML before the Suspense and stream it down
|
||||
_ = tx.try_send(std::mem::take(&mut buffer));
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send async HTML chunk");
|
||||
|
||||
// send the inner stream
|
||||
let suspended = suspended.await;
|
||||
@@ -138,7 +143,8 @@ async fn handle_chunks(mut tx: Sender<String>, chunks: Vec<StreamChunk>) {
|
||||
}
|
||||
}
|
||||
// send final sync chunk
|
||||
_ = tx.try_send(std::mem::take(&mut buffer));
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send final HTML chunk");
|
||||
}
|
||||
|
||||
impl View {
|
||||
@@ -186,8 +192,17 @@ impl View {
|
||||
format!("<!--leptos-view|{id}|open-->").into(),
|
||||
));
|
||||
}
|
||||
if let Some(prerendered) = el.prerendered {
|
||||
chunks.push(StreamChunk::Sync(prerendered))
|
||||
if let ElementChildren::Chunks(el_chunks) = el.children {
|
||||
for chunk in el_chunks {
|
||||
match chunk {
|
||||
StringOrView::String(string) => {
|
||||
chunks.push(StreamChunk::Sync(string))
|
||||
}
|
||||
StringOrView::View(view) => {
|
||||
view().into_stream_chunks_helper(cx, chunks);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
|
||||
@@ -231,8 +246,19 @@ impl View {
|
||||
chunks.push(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}>").into(),
|
||||
));
|
||||
for child in el.children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
|
||||
match el.children {
|
||||
ElementChildren::Empty => {}
|
||||
ElementChildren::Children(children) => {
|
||||
for child in children {
|
||||
child.into_stream_chunks_helper(cx, chunks);
|
||||
}
|
||||
}
|
||||
ElementChildren::InnerHtml(inner_html) => {
|
||||
chunks.push(StreamChunk::Sync(inner_html));
|
||||
}
|
||||
// handled above
|
||||
ElementChildren::Chunks(_) => unreachable!(),
|
||||
}
|
||||
|
||||
chunks.push(StreamChunk::Sync(
|
||||
|
||||
@@ -2,10 +2,6 @@ use crate::node::{LAttributeValue, LNode};
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// TODO: insertion and removal code are still somewhat broken
|
||||
// namely, it will tend to remove and move or mutate nodes,
|
||||
// which causes a bit of a problem for DynChild etc.
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct OldChildren(IndexMap<LNode, Vec<usize>>);
|
||||
|
||||
@@ -58,7 +54,7 @@ impl LNode {
|
||||
.collect(),
|
||||
},
|
||||
LNode::Text(_)
|
||||
| LNode::Component(_, _)
|
||||
| LNode::Component { .. }
|
||||
| LNode::DynChild(_) => ReplacementNode::Html(self.to_html()),
|
||||
},
|
||||
}
|
||||
@@ -80,10 +76,19 @@ impl LNode {
|
||||
child.add_old_children(new_path, positions);
|
||||
}
|
||||
}
|
||||
// only need to insert dynamic content, as these might change
|
||||
LNode::Component(_, _) | LNode::DynChild(_) => {
|
||||
// need to insert dynamic content and children, as these might change
|
||||
LNode::DynChild(_) => {
|
||||
positions.0.insert(self.clone(), path);
|
||||
}
|
||||
LNode::Component { children, .. } => {
|
||||
positions.0.insert(self.clone(), path.clone());
|
||||
|
||||
for (idx, child) in children.iter().enumerate() {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(idx);
|
||||
child.add_old_children(new_path, positions);
|
||||
}
|
||||
}
|
||||
// can just create text nodes, whatever
|
||||
LNode::Text(_) => {}
|
||||
}
|
||||
@@ -148,6 +153,28 @@ impl LNode {
|
||||
.collect()
|
||||
}
|
||||
// components + dynamic context: no patches
|
||||
(
|
||||
LNode::Component {
|
||||
name: old_name,
|
||||
children: old_children,
|
||||
..
|
||||
},
|
||||
LNode::Component {
|
||||
name: new_name,
|
||||
children: new_children,
|
||||
..
|
||||
},
|
||||
) if old_name == new_name => {
|
||||
let mut path = path.to_vec();
|
||||
path.push(0);
|
||||
path.push(0);
|
||||
LNode::diff_children(
|
||||
&path,
|
||||
old_children,
|
||||
new_children,
|
||||
orig_children,
|
||||
)
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ pub enum LNode {
|
||||
},
|
||||
// don't need anything; skipped during patching because it should
|
||||
// contain its own view macros
|
||||
Component(String, Vec<(String, String)>),
|
||||
Component {
|
||||
name: String,
|
||||
props: Vec<(String, String)>,
|
||||
children: Vec<LNode>,
|
||||
},
|
||||
DynChild(String),
|
||||
}
|
||||
|
||||
@@ -71,9 +75,14 @@ impl LNode {
|
||||
}
|
||||
Node::Element(el) => {
|
||||
if is_component_node(&el) {
|
||||
views.push(LNode::Component(
|
||||
el.name.to_string(),
|
||||
el.attributes
|
||||
let mut children = Vec::new();
|
||||
for child in el.children {
|
||||
LNode::parse_node(child, &mut children)?;
|
||||
}
|
||||
views.push(LNode::Component {
|
||||
name: el.name.to_string(),
|
||||
props: el
|
||||
.attributes
|
||||
.into_iter()
|
||||
.filter_map(|attr| match attr {
|
||||
Node::Attribute(attr) => Some((
|
||||
@@ -83,7 +92,8 @@ impl LNode {
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
children,
|
||||
});
|
||||
} else {
|
||||
let name = el.name.to_string();
|
||||
let mut attrs = Vec::new();
|
||||
@@ -125,7 +135,7 @@ impl LNode {
|
||||
match self {
|
||||
LNode::Fragment(frag) => frag.iter().map(LNode::to_html).collect(),
|
||||
LNode::Text(text) => text.to_owned(),
|
||||
LNode::Component(name, _) => format!(
|
||||
LNode::Component { name, .. } => format!(
|
||||
"<!--<{name}>--><pre><{name}/> will load once Rust code \
|
||||
has been compiled.</pre><!--</{name}>-->"
|
||||
),
|
||||
|
||||
@@ -2,7 +2,8 @@ console.log("[HOT RELOADING] Connected to server.");
|
||||
function patch(json) {
|
||||
try {
|
||||
const views = JSON.parse(json);
|
||||
for (const [id, patches] of views) {
|
||||
for (const [id, patches] of views) {
|
||||
console.log("[HOT RELOAD]", patches);
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
|
||||
open = `leptos-view|${id}|open`,
|
||||
close = `leptos-view|${id}|close`;
|
||||
@@ -250,7 +251,33 @@ function patch(json) {
|
||||
node: walker.currentNode
|
||||
});
|
||||
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
|
||||
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
|
||||
if (walker.currentNode.textContent.trim().endsWith("-children|open")) {
|
||||
const startingName = walker.currentNode.textContent.trim();
|
||||
const componentName = startingName.replace("-children|open").replace("leptos-view|");
|
||||
const endingName = `leptos-view|${componentName}-children|close`;
|
||||
let start = walker.currentNode;
|
||||
let depth = 1;
|
||||
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.textContent.trim() == endingName) {
|
||||
depth--;
|
||||
} else if (walker.currentNode.textContent.trim() == startingName) {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if(depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = walker.currentNode;
|
||||
actualChildren.push({
|
||||
type: "fragment",
|
||||
start: start.nextSibling,
|
||||
end: end.previousSibling,
|
||||
children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling)
|
||||
});
|
||||
}
|
||||
} else if (walker.currentNode.textContent.trim() == "<() />") {
|
||||
actualChildren.push({
|
||||
type: "unit",
|
||||
@@ -326,7 +353,7 @@ function patch(json) {
|
||||
return actualChildren;
|
||||
}
|
||||
|
||||
function childAtPath(element, path) {
|
||||
function childAtPath(element, path) {
|
||||
if (path.length == 0) {
|
||||
return element;
|
||||
} else if (element.children) {
|
||||
|
||||
@@ -22,9 +22,7 @@ proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full"] }
|
||||
syn-rsx = "0.9"
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_hot_reload = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = "0.6.0"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
@@ -37,10 +35,10 @@ leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
csr = ["leptos_dom/web", "leptos_reactive/csr"]
|
||||
hydrate = ["leptos_dom/web", "leptos_reactive/hydrate"]
|
||||
ssr = ["leptos_dom/ssr", "leptos_reactive/ssr"]
|
||||
stable = ["leptos_dom/stable", "leptos_reactive/stable"]
|
||||
csr = []
|
||||
hydrate = []
|
||||
ssr = []
|
||||
stable = ["server_fn_macro/stable"]
|
||||
tracing = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -267,32 +267,55 @@ fn root_element_to_tokens_ssr(
|
||||
if is_component_node(node) {
|
||||
component_to_tokens(cx, node, global_class)
|
||||
} else {
|
||||
let mut template = String::new();
|
||||
let mut holes = Vec::<TokenStream>::new();
|
||||
let mut exprs_for_compiler = Vec::<TokenStream>::new();
|
||||
|
||||
let mut template = String::new();
|
||||
let mut holes = Vec::new();
|
||||
let mut chunks = Vec::new();
|
||||
element_to_tokens_ssr(
|
||||
cx,
|
||||
node,
|
||||
&mut template,
|
||||
&mut holes,
|
||||
&mut chunks,
|
||||
&mut exprs_for_compiler,
|
||||
true,
|
||||
global_class,
|
||||
);
|
||||
|
||||
let template = if holes.is_empty() {
|
||||
quote! {
|
||||
#template
|
||||
// push final chunk
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String { template, holes })
|
||||
}
|
||||
|
||||
let chunks = chunks.into_iter().map(|chunk| match chunk {
|
||||
SsrElementChunks::String { template, holes } => {
|
||||
if holes.is_empty() {
|
||||
quote! {
|
||||
leptos::leptos_dom::html::StringOrView::String(#template.into())
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
leptos::leptos_dom::html::StringOrView::String(
|
||||
format!(
|
||||
#template,
|
||||
#(#holes),*
|
||||
)
|
||||
.into()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
format!(
|
||||
#template,
|
||||
#(#holes)*
|
||||
)
|
||||
}
|
||||
};
|
||||
SsrElementChunks::View(view) => {
|
||||
quote! {
|
||||
#[allow(unused_braces)]
|
||||
{
|
||||
let view = #view;
|
||||
leptos::leptos_dom::html::StringOrView::View(std::rc::Rc::new(move || view.clone()))
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
let is_custom_element = is_custom_element(&tag_name);
|
||||
@@ -328,27 +351,41 @@ fn root_element_to_tokens_ssr(
|
||||
quote! {
|
||||
{
|
||||
#(#exprs_for_compiler)*
|
||||
::leptos::HtmlElement::from_html(cx, #full_name, #template)#view_marker
|
||||
::leptos::HtmlElement::from_chunks(#cx, #full_name, [#(#chunks),*])#view_marker
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SsrElementChunks {
|
||||
String {
|
||||
template: String,
|
||||
holes: Vec<TokenStream>,
|
||||
},
|
||||
View(TokenStream),
|
||||
}
|
||||
|
||||
fn element_to_tokens_ssr(
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
template: &mut String,
|
||||
holes: &mut Vec<TokenStream>,
|
||||
chunks: &mut Vec<SsrElementChunks>,
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
is_root: bool,
|
||||
global_class: Option<&TokenTree>,
|
||||
) {
|
||||
if is_component_node(node) {
|
||||
template.push_str("{}");
|
||||
let component = component_to_tokens(cx, node, global_class);
|
||||
holes.push(quote! {
|
||||
{#component}.into_view(cx).render_to_string(cx),
|
||||
})
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String {
|
||||
template: std::mem::take(template),
|
||||
holes: std::mem::take(holes),
|
||||
})
|
||||
}
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#component}.into_view(#cx)
|
||||
}));
|
||||
} else {
|
||||
let tag_name = node
|
||||
.name
|
||||
@@ -374,9 +411,9 @@ fn element_to_tokens_ssr(
|
||||
|
||||
// insert hydration ID
|
||||
let hydration_id = if is_root {
|
||||
quote! { leptos::leptos_dom::HydrationCtx::peek(), }
|
||||
quote! { leptos::leptos_dom::HydrationCtx::peek() }
|
||||
} else {
|
||||
quote! { leptos::leptos_dom::HydrationCtx::id(), }
|
||||
quote! { leptos::leptos_dom::HydrationCtx::id() }
|
||||
};
|
||||
match node
|
||||
.attributes
|
||||
@@ -404,20 +441,23 @@ fn element_to_tokens_ssr(
|
||||
let value = inner_html.as_ref();
|
||||
|
||||
holes.push(quote! {
|
||||
(#value).into_attribute(cx).as_nameless_value_string().unwrap_or_default(),
|
||||
(#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default()
|
||||
})
|
||||
} else {
|
||||
for child in &node.children {
|
||||
match child {
|
||||
Node::Element(child) => element_to_tokens_ssr(
|
||||
cx,
|
||||
child,
|
||||
template,
|
||||
holes,
|
||||
exprs_for_compiler,
|
||||
false,
|
||||
global_class,
|
||||
),
|
||||
Node::Element(child) => {
|
||||
element_to_tokens_ssr(
|
||||
cx,
|
||||
child,
|
||||
template,
|
||||
holes,
|
||||
chunks,
|
||||
exprs_for_compiler,
|
||||
false,
|
||||
global_class,
|
||||
);
|
||||
}
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
template.push_str(&html_escape::encode_safe(
|
||||
@@ -428,7 +468,7 @@ fn element_to_tokens_ssr(
|
||||
let value = text.value.as_ref();
|
||||
|
||||
holes.push(quote! {
|
||||
#value.into_view(#cx).render_to_string(#cx),
|
||||
#value.into_view(#cx).render_to_string(#cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -436,14 +476,23 @@ fn element_to_tokens_ssr(
|
||||
if let Some(value) = value_to_string(&block.value) {
|
||||
template.push_str(&value);
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
let value = block.value.as_ref();
|
||||
holes.push(quote! {
|
||||
#value.into_view(#cx).render_to_string(#cx),
|
||||
})
|
||||
|
||||
if !template.is_empty() {
|
||||
chunks.push(SsrElementChunks::String {
|
||||
template: std::mem::take(template),
|
||||
holes: std::mem::take(holes),
|
||||
})
|
||||
}
|
||||
chunks.push(SsrElementChunks::View(quote! {
|
||||
{#value}.into_view(#cx)
|
||||
}));
|
||||
}
|
||||
}
|
||||
Node::Fragment(_) => todo!(),
|
||||
Node::Fragment(_) => abort!(
|
||||
Span::call_site(),
|
||||
"You can't nest a fragment inside an element."
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -498,7 +547,7 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
&{#value}.into_attribute(#cx)
|
||||
.as_nameless_value_string()
|
||||
.map(|a| format!("{}=\"{}\"", #name, leptos::leptos_dom::ssr::escape_attr(&a)))
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -618,9 +667,9 @@ fn set_class_attribute_ssr(
|
||||
template.push_str(" {}");
|
||||
let value = value.as_ref();
|
||||
holes.push(quote! {
|
||||
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
|
||||
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_default()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -628,13 +677,13 @@ fn set_class_attribute_ssr(
|
||||
for (_span, name, value) in &class_attrs {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! {
|
||||
(cx, #value).into_class(#cx).as_value_string(#name),
|
||||
(#cx, #value).into_class(#cx).as_value_string(#name)
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(dyn_global_class) = dyn_global_class {
|
||||
template.push_str(" {}");
|
||||
holes.push(quote! { #dyn_global_class, });
|
||||
holes.push(quote! { #dyn_global_class });
|
||||
}
|
||||
|
||||
template.push('"');
|
||||
@@ -1053,6 +1102,15 @@ pub(crate) fn component_to_tokens(
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let marker = format!("<{component_name}/>-children");
|
||||
let view_marker = quote! { .with_view_marker(#marker) };
|
||||
} else {
|
||||
let view_marker = quote! {};
|
||||
}
|
||||
}
|
||||
|
||||
let children = fragment_to_tokens(
|
||||
cx,
|
||||
span,
|
||||
@@ -1071,7 +1129,7 @@ pub(crate) fn component_to_tokens(
|
||||
.children({
|
||||
#(#clonables)*
|
||||
|
||||
Box::new(move |#cx| #children)
|
||||
Box::new(move |#cx| #children #view_marker)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,6 +10,11 @@ fn Component(
|
||||
#[prop(default = NonZeroUsize::new(10).unwrap())] default: NonZeroUsize,
|
||||
#[prop(into)] into: String,
|
||||
) -> impl IntoView {
|
||||
_ = optional;
|
||||
_ = optional_no_strip;
|
||||
_ = strip_option;
|
||||
_ = default;
|
||||
_ = into;
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,6 +24,7 @@ bytecheck = { version = "0.7", features = [
|
||||
"uuid",
|
||||
"simdutf8",
|
||||
], optional = true }
|
||||
rustc-hash = "1"
|
||||
serde-wasm-bindgen = "0.5"
|
||||
serde_json = "1"
|
||||
base64 = "0.21"
|
||||
@@ -40,6 +41,7 @@ web-sys = { version = "0.3", features = [
|
||||
"Window",
|
||||
] }
|
||||
cfg-if = "1.0.0"
|
||||
indexmap = "1.9.2"
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4"
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use crate::{runtime::with_runtime, Scope};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
|
||||
/// Provides a context value of type `T` to the current reactive [Scope](crate::Scope)
|
||||
/// and all of its descendants. This can be consumed using [use_context](crate::use_context).
|
||||
@@ -57,8 +55,7 @@ where
|
||||
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut contexts = runtime.scope_contexts.borrow_mut();
|
||||
let context =
|
||||
contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
|
||||
let context = contexts.entry(cx.id).unwrap().or_default();
|
||||
context.insert(id, Box::new(value) as Box<dyn Any>);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
macros::debug_warn,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
Runtime, Scope, ScopeProperty,
|
||||
};
|
||||
use crate::{Scope, ScopeProperty};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{cell::RefCell, fmt::Debug};
|
||||
use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
|
||||
/// Effects run a certain chunk of code whenever the signals they depend on change.
|
||||
/// `create_effect` immediately runs the given function once, tracks its dependence
|
||||
@@ -69,6 +65,7 @@ where
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "ssr"))] {
|
||||
let e = cx.runtime.create_effect(f);
|
||||
//eprintln!("created effect {e:?}");
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
|
||||
} else {
|
||||
// clear warnings
|
||||
@@ -123,6 +120,7 @@ pub fn create_isomorphic_effect<T>(
|
||||
T: 'static,
|
||||
{
|
||||
let e = cx.runtime.create_effect(f);
|
||||
//eprintln!("created effect {e:?}");
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
|
||||
}
|
||||
|
||||
@@ -145,27 +143,22 @@ where
|
||||
create_effect(cx, f);
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to an [Effect](crate::Effect).
|
||||
pub(crate) struct EffectId;
|
||||
}
|
||||
|
||||
pub(crate) struct Effect<T, F>
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> T,
|
||||
{
|
||||
pub(crate) f: F,
|
||||
pub(crate) value: RefCell<Option<T>>,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
pub(crate) trait AnyEffect {
|
||||
fn run(&self, id: EffectId, runtime: RuntimeId);
|
||||
pub(crate) trait AnyComputation {
|
||||
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool;
|
||||
}
|
||||
|
||||
impl<T, F> AnyEffect for Effect<T, F>
|
||||
impl<T, F> AnyComputation for Effect<T, F>
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> T,
|
||||
@@ -177,73 +170,34 @@ where
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn run(&self, id: EffectId, runtime: RuntimeId) {
|
||||
_ = with_runtime(runtime, |runtime| {
|
||||
// clear previous dependencies
|
||||
id.cleanup(runtime);
|
||||
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
|
||||
// we defensively take and release the BorrowMut twice here
|
||||
// in case a change during the effect running schedules a rerun
|
||||
// ideally this should never happen, but this guards against panic
|
||||
let curr_value = {
|
||||
// downcast value
|
||||
let mut value = value.borrow_mut();
|
||||
let value = value
|
||||
.downcast_mut::<Option<T>>()
|
||||
.expect("to downcast effect value");
|
||||
value.take()
|
||||
};
|
||||
|
||||
// set this as the current observer
|
||||
let prev_observer = runtime.observer.take();
|
||||
runtime.observer.set(Some(id));
|
||||
// run the effect
|
||||
let new_value = (self.f)(curr_value);
|
||||
|
||||
// run the effect
|
||||
let value = self.value.take();
|
||||
let new_value = (self.f)(value);
|
||||
*self.value.borrow_mut() = Some(new_value);
|
||||
// set new value
|
||||
let mut value = value.borrow_mut();
|
||||
let value = value
|
||||
.downcast_mut::<Option<T>>()
|
||||
.expect("to downcast effect value");
|
||||
*value = Some(new_value);
|
||||
|
||||
// restore the previous observer
|
||||
runtime.observer.set(prev_observer);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectId {
|
||||
pub(crate) fn run(&self, runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
effects.get(*self).cloned()
|
||||
};
|
||||
if let Some(effect) = effect {
|
||||
effect.run(*self, runtime_id);
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Effect] Trying to run an Effect that has been disposed. \
|
||||
This is probably either a logic error in a component \
|
||||
that creates and disposes of scopes, or a Resource \
|
||||
resolving after its scope has been dropped without \
|
||||
having been cleaned up."
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Effect::cleanup()",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self,
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub(crate) fn cleanup(&self, runtime: &Runtime) {
|
||||
let sources = runtime.effect_sources.borrow();
|
||||
if let Some(sources) = sources.get(*self) {
|
||||
let subs = runtime.signal_subscribers.borrow();
|
||||
for source in sources.borrow().iter() {
|
||||
if let Some(source) = subs.get(*source) {
|
||||
source.borrow_mut().remove(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ mod context;
|
||||
mod effect;
|
||||
mod hydration;
|
||||
mod memo;
|
||||
mod node;
|
||||
mod resource;
|
||||
mod runtime;
|
||||
mod scope;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_effect, on_cleanup, ReadSignal, Scope, SignalGet,
|
||||
SignalGetUntracked, SignalStream, SignalWith, SignalWithUntracked,
|
||||
create_effect, node::NodeId, on_cleanup, with_runtime, AnyComputation,
|
||||
RuntimeId, Scope, SignalDispose, SignalGet, SignalGetUntracked,
|
||||
SignalStream, SignalWith, SignalWithUntracked,
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
|
||||
|
||||
/// Creates an efficient derived reactive value based on other reactive values.
|
||||
///
|
||||
@@ -64,7 +65,8 @@ use std::fmt::Debug;
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = ?cx.id,
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
@@ -145,23 +147,29 @@ where
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Memo<T>(
|
||||
pub(crate) ReadSignal<Option<T>>,
|
||||
#[cfg(debug_assertions)] pub(crate) &'static std::panic::Location<'static>,
|
||||
)
|
||||
pub struct Memo<T>
|
||||
where
|
||||
T: 'static;
|
||||
T: 'static,
|
||||
{
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Memo<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self(
|
||||
self.0,
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
self.1,
|
||||
)
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,16 +183,23 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
name = "Memo::get_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T {
|
||||
// Unwrapping is fine because `T` will already be `Some(T)` by
|
||||
// the time this method can be called
|
||||
self.0.get_untracked().unwrap()
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
match self.id.try_with_no_subscription(runtime, T::clone) {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at,
|
||||
),
|
||||
}
|
||||
})
|
||||
.expect("runtime to be alive")
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -194,14 +209,18 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
name = "Memo::try_get_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn try_get_untracked(&self) -> Option<T> {
|
||||
self.0.try_get_untracked().flatten()
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
self.id.try_with_no_subscription(runtime, T::clone).ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,8 +232,8 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
||||
name = "Memo::with_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
@@ -222,7 +241,16 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
// Unwrapping here is fine for the same reasons as <Memo as
|
||||
// UntrackedSignal>::get_untracked
|
||||
self.0.with_untracked(|v| f(v.as_ref().unwrap()))
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
match self.id.try_with_no_subscription(runtime, |v: &T| f(v)) {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at,
|
||||
),
|
||||
}
|
||||
})
|
||||
.expect("runtime to be alive")
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -232,14 +260,18 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
||||
name = "Memo::try_with_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0.try_with_untracked(|t| f(t.as_ref().unwrap()))
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
self.id.try_with_no_subscription(runtime, |v: &T| f(v)).ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,13 +299,14 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get(&self) -> T {
|
||||
self.0.get().unwrap()
|
||||
self.with(T::clone)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -283,14 +316,14 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
||||
name = "Memo::try_get()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn try_get(&self) -> Option<T> {
|
||||
self.0.try_get().flatten()
|
||||
self.try_with(T::clone)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,14 +335,20 @@ impl<T> SignalWith<T> for Memo<T> {
|
||||
name = "Memo::with()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
self.0.with(|t| f(t.as_ref().unwrap()))
|
||||
match self.try_with(f) {
|
||||
Some(t) => t,
|
||||
None => panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -319,18 +358,40 @@ impl<T> SignalWith<T> for Memo<T> {
|
||||
name = "Memo::try_with()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0.try_with(|t| f(t.as_ref().unwrap())).ok()
|
||||
// memo is stored as Option<T>, but will always have T available
|
||||
// after latest_value() called, so we can unwrap safely
|
||||
let f = move |maybe_value: &Option<T>| f(maybe_value.as_ref().unwrap());
|
||||
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
self.id.subscribe(runtime);
|
||||
self.id.try_with_no_subscription(runtime, f).ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalStream<T> for Memo<T> {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Memo::to_stream()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn to_stream(
|
||||
&self,
|
||||
cx: Scope,
|
||||
@@ -351,14 +412,98 @@ impl<T: Clone> SignalStream<T> for Memo<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Memo<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) fn subscribe(&self) {
|
||||
self.0.subscribe()
|
||||
impl<T> SignalDispose for Memo<T> {
|
||||
fn dispose(self) {
|
||||
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
|
||||
}
|
||||
}
|
||||
|
||||
impl_get_fn_traits![Memo];
|
||||
|
||||
pub(crate) struct MemoState<T, F>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
F: Fn(Option<&T>) -> T,
|
||||
{
|
||||
pub f: F,
|
||||
pub t: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T, F> AnyComputation for MemoState<T, F>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
F: Fn(Option<&T>) -> T,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Memo::run()",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
|
||||
let (new_value, is_different) = {
|
||||
let value = value.borrow();
|
||||
let curr_value = value
|
||||
.downcast_ref::<Option<T>>()
|
||||
.expect("to downcast memo value");
|
||||
|
||||
// run the effect
|
||||
let new_value = (self.f)(curr_value.as_ref());
|
||||
let is_different = curr_value.as_ref() != Some(&new_value);
|
||||
(new_value, is_different)
|
||||
};
|
||||
if is_different {
|
||||
let mut value = value.borrow_mut();
|
||||
let curr_value = value
|
||||
.downcast_mut::<Option<T>>()
|
||||
.expect("to downcast memo value");
|
||||
*curr_value = Some(new_value);
|
||||
}
|
||||
|
||||
is_different
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn format_memo_warning(
|
||||
msg: &str,
|
||||
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<'static>,
|
||||
) -> String {
|
||||
let location = std::panic::Location::caller();
|
||||
|
||||
let defined_at_msg = {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!("signal created here: {defined_at}\n")
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
String::default()
|
||||
}
|
||||
};
|
||||
|
||||
format!("{msg}\n{defined_at_msg}warning happened here: {location}",)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn panic_getting_dead_memo(
|
||||
#[cfg(debug_assertions)] defined_at: &'static std::panic::Location<'static>,
|
||||
) -> ! {
|
||||
panic!(
|
||||
"{}",
|
||||
format_memo_warning(
|
||||
"Attempted to get a memo after it was disposed.",
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
28
leptos_reactive/src/node.rs
Normal file
28
leptos_reactive/src/node.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use crate::AnyComputation;
|
||||
use std::{any::Any, cell::RefCell, rc::Rc};
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a signal.
|
||||
pub struct NodeId;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ReactiveNode {
|
||||
pub value: Rc<RefCell<dyn Any>>,
|
||||
pub state: ReactiveNodeState,
|
||||
pub node_type: ReactiveNodeType,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ReactiveNodeType {
|
||||
Signal,
|
||||
Memo { f: Rc<dyn AnyComputation> },
|
||||
Effect { f: Rc<dyn AnyComputation> },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub(crate) enum ReactiveNodeState {
|
||||
Clean,
|
||||
Check,
|
||||
Dirty,
|
||||
}
|
||||
@@ -115,6 +115,7 @@ where
|
||||
|
||||
let (loading, set_loading) = create_signal(cx, false);
|
||||
|
||||
//crate::macros::debug_warn!("creating fetcher");
|
||||
let fetcher = Rc::new(move |s| {
|
||||
Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>
|
||||
});
|
||||
@@ -139,6 +140,7 @@ where
|
||||
})
|
||||
.expect("tried to create a Resource in a Runtime that has been disposed.");
|
||||
|
||||
//crate::macros::debug_warn!("creating effect");
|
||||
create_isomorphic_effect(cx, {
|
||||
let r = Rc::clone(&r);
|
||||
move |_| {
|
||||
@@ -222,7 +224,6 @@ where
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_local_resource_with_initial_value<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> S + 'static,
|
||||
@@ -318,7 +319,7 @@ where
|
||||
r.set_loading.update(|n| *n = false);
|
||||
|
||||
// for reactivity
|
||||
r.source.subscribe();
|
||||
r.source.track();
|
||||
} else if context.pending_resources.remove(&id) {
|
||||
// We're still waiting for the resource, add a "resolver" closure so
|
||||
// that it will be set as soon as the server sends the serialized
|
||||
@@ -356,7 +357,7 @@ where
|
||||
);
|
||||
|
||||
// for reactivity
|
||||
r.source.subscribe()
|
||||
r.source.track()
|
||||
} else {
|
||||
// Server didn't mark the resource as pending, so load it on the
|
||||
// client
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo,
|
||||
ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer,
|
||||
ScopeId, ScopeProperty, SerializableResource, SignalId, SignalUpdate,
|
||||
UnserializableResource, WriteSignal,
|
||||
hydration::SharedContext,
|
||||
node::{NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType},
|
||||
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
|
||||
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
|
||||
ScopeProperty, SerializableResource, StoredValueId, UnserializableResource,
|
||||
WriteSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use core::hash::BuildHasherDefault;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use indexmap::IndexSet;
|
||||
use rustc_hash::{FxHashMap, FxHasher};
|
||||
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
@@ -33,6 +37,247 @@ cfg_if! {
|
||||
}
|
||||
}
|
||||
|
||||
type FxIndexSet<T> = IndexSet<T, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
// The data structure that owns all the signals, memos, effects,
|
||||
// and other data included in the reactive system.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Runtime {
|
||||
pub shared_context: RefCell<SharedContext>,
|
||||
pub observer: Cell<Option<NodeId>>,
|
||||
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
|
||||
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
|
||||
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_contexts:
|
||||
RefCell<SparseSecondaryMap<ScopeId, FxHashMap<TypeId, Box<dyn Any>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_cleanups:
|
||||
RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
|
||||
pub stored_values: RefCell<SlotMap<StoredValueId, Rc<RefCell<dyn Any>>>>,
|
||||
pub nodes: RefCell<SlotMap<NodeId, ReactiveNode>>,
|
||||
pub node_subscribers:
|
||||
RefCell<SecondaryMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
|
||||
pub node_sources:
|
||||
RefCell<SecondaryMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
|
||||
pub pending_effects: RefCell<Vec<NodeId>>,
|
||||
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
|
||||
pub batching: Cell<bool>,
|
||||
}
|
||||
|
||||
// This core Runtime impl block handles all the work of marking and updating
|
||||
// the reactive graph.
|
||||
//
|
||||
// In terms of concept and algorithm, this reactive-system implementation
|
||||
// is significantly inspired by Reactively (https://github.com/modderme123/reactively)
|
||||
impl Runtime {
|
||||
pub(crate) fn update_if_necessary(&self, node_id: NodeId) {
|
||||
//crate::macros::debug_warn!("update_if_necessary {node_id:?}");
|
||||
if self.current_state(node_id) == ReactiveNodeState::Check {
|
||||
let sources = {
|
||||
let sources = self.node_sources.borrow();
|
||||
sources.get(node_id).map(|n| n.borrow().clone())
|
||||
};
|
||||
for source in sources.into_iter().flatten() {
|
||||
self.update_if_necessary(source);
|
||||
if self.current_state(node_id) == ReactiveNodeState::Dirty {
|
||||
// as soon as a single parent has marked us dirty, we can
|
||||
// stop checking them to avoid over-re-running
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're dirty at this point, update
|
||||
if self.current_state(node_id) == ReactiveNodeState::Dirty {
|
||||
self.update(node_id);
|
||||
}
|
||||
|
||||
// now we're clean
|
||||
self.mark_clean(node_id);
|
||||
}
|
||||
|
||||
pub(crate) fn update(&self, node_id: NodeId) {
|
||||
//crate::macros::debug_warn!("updating {node_id:?}");
|
||||
let node = {
|
||||
let nodes = self.nodes.borrow();
|
||||
nodes.get(node_id).cloned()
|
||||
};
|
||||
let subs = {
|
||||
let subs = self.node_subscribers.borrow();
|
||||
subs.get(node_id).cloned()
|
||||
};
|
||||
if let Some(node) = node {
|
||||
// memos and effects rerun
|
||||
// signals simply have their value
|
||||
let changed = match node.node_type {
|
||||
ReactiveNodeType::Signal => true,
|
||||
ReactiveNodeType::Memo { f }
|
||||
| ReactiveNodeType::Effect { f } => {
|
||||
// set this node as the observer
|
||||
self.with_observer(node_id, move || {
|
||||
// clean up sources of this memo/effect
|
||||
self.cleanup(node_id);
|
||||
|
||||
f.run(Rc::clone(&node.value))
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// mark children dirty
|
||||
if changed {
|
||||
if let Some(subs) = subs {
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
for sub_id in subs.borrow().iter() {
|
||||
if let Some(sub) = nodes.get_mut(*sub_id) {
|
||||
//crate::macros::debug_warn!(
|
||||
// "update is marking {sub_id:?} dirty"
|
||||
//);
|
||||
sub.state = ReactiveNodeState::Dirty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mark clean
|
||||
self.mark_clean(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup(&self, node_id: NodeId) {
|
||||
let sources = self.node_sources.borrow();
|
||||
if let Some(sources) = sources.get(node_id) {
|
||||
let subs = self.node_subscribers.borrow();
|
||||
for source in sources.borrow().iter() {
|
||||
if let Some(source) = subs.get(*source) {
|
||||
source.borrow_mut().remove(&node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_state(&self, node: NodeId) -> ReactiveNodeState {
|
||||
match self.nodes.borrow().get(node) {
|
||||
None => ReactiveNodeState::Clean,
|
||||
Some(node) => node.state,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_observer<T>(&self, observer: NodeId, f: impl FnOnce() -> T) -> T {
|
||||
let prev_observer = self.observer.take();
|
||||
self.observer.set(Some(observer));
|
||||
let v = f();
|
||||
self.observer.set(prev_observer);
|
||||
v
|
||||
}
|
||||
|
||||
fn mark_clean(&self, node: NodeId) {
|
||||
//crate::macros::debug_warn!("marking {node:?} clean");
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
if let Some(node) = nodes.get_mut(node) {
|
||||
node.state = ReactiveNodeState::Clean;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mark_dirty(&self, node: NodeId) {
|
||||
//crate::macros::debug_warn!("marking {node:?} dirty");
|
||||
let mut nodes = self.nodes.borrow_mut();
|
||||
let mut pending_effects = self.pending_effects.borrow_mut();
|
||||
let subscribers = self.node_subscribers.borrow();
|
||||
let current_observer = self.observer.get();
|
||||
|
||||
// mark self dirty
|
||||
if let Some(current_node) = nodes.get_mut(node) {
|
||||
Runtime::mark(
|
||||
node,
|
||||
current_node,
|
||||
ReactiveNodeState::Dirty,
|
||||
&mut pending_effects,
|
||||
current_observer,
|
||||
);
|
||||
|
||||
// mark all children check
|
||||
// this can probably be done in a better way
|
||||
let mut descendants = Default::default();
|
||||
Runtime::gather_descendants(&subscribers, node, &mut descendants);
|
||||
for descendant in descendants {
|
||||
if let Some(node) = nodes.get_mut(descendant) {
|
||||
Runtime::mark(
|
||||
descendant,
|
||||
node,
|
||||
ReactiveNodeState::Check,
|
||||
&mut pending_effects,
|
||||
current_observer,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mark(
|
||||
//nodes: &mut SlotMap<NodeId, ReactiveNode>,
|
||||
node_id: NodeId,
|
||||
node: &mut ReactiveNode,
|
||||
level: ReactiveNodeState,
|
||||
pending_effects: &mut Vec<NodeId>,
|
||||
current_observer: Option<NodeId>,
|
||||
) {
|
||||
//crate::macros::debug_warn!("marking {node_id:?} {level:?}");
|
||||
if level > node.state {
|
||||
node.state = level;
|
||||
}
|
||||
if matches!(node.node_type, ReactiveNodeType::Effect { .. })
|
||||
&& current_observer != Some(node_id)
|
||||
{
|
||||
//crate::macros::debug_warn!("pushing effect {node_id:?}");
|
||||
pending_effects.push(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn gather_descendants(
|
||||
subscribers: &SecondaryMap<NodeId, RefCell<FxIndexSet<NodeId>>>,
|
||||
node: NodeId,
|
||||
descendants: &mut FxIndexSet<NodeId>,
|
||||
) {
|
||||
if let Some(children) = subscribers.get(node) {
|
||||
for child in children.borrow().iter() {
|
||||
descendants.insert(*child);
|
||||
Runtime::gather_descendants(subscribers, *child, descendants);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run_effects(runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
runtime.run_your_effects();
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn run_your_effects(&self) {
|
||||
let effects = self.pending_effects.take();
|
||||
for effect_id in effects {
|
||||
self.update_if_necessary(effect_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dispose_node(&self, node: NodeId) {
|
||||
self.node_sources.borrow_mut().remove(node);
|
||||
self.node_subscribers.borrow_mut().remove(node);
|
||||
self.nodes.borrow_mut().remove(node);
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Runtime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Runtime")
|
||||
.field("shared_context", &self.shared_context)
|
||||
.field("observer", &self.observer)
|
||||
.field("scopes", &self.scopes)
|
||||
.field("scope_parents", &self.scope_parents)
|
||||
.field("scope_children", &self.scope_children)
|
||||
.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.
|
||||
pub(crate) fn with_runtime<T>(
|
||||
@@ -130,11 +375,15 @@ impl RuntimeId {
|
||||
pub(crate) fn create_concrete_signal(
|
||||
self,
|
||||
value: Rc<RefCell<dyn Any>>,
|
||||
) -> SignalId {
|
||||
with_runtime(self, |runtime| runtime.signals.borrow_mut().insert(value))
|
||||
.expect(
|
||||
"tried to create a signal in a runtime that has been disposed",
|
||||
)
|
||||
) -> NodeId {
|
||||
with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value,
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Signal,
|
||||
})
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -178,7 +427,7 @@ impl RuntimeId {
|
||||
T: Any + 'static,
|
||||
{
|
||||
with_runtime(self, move |runtime| {
|
||||
let mut signals = runtime.signals.borrow_mut();
|
||||
let mut signals = runtime.nodes.borrow_mut();
|
||||
let properties = runtime.scopes.borrow();
|
||||
let mut properties = properties
|
||||
.get(cx.id)
|
||||
@@ -191,7 +440,13 @@ impl RuntimeId {
|
||||
signals.reserve(size);
|
||||
properties.reserve(size);
|
||||
values
|
||||
.map(|value| signals.insert(Rc::new(RefCell::new(value))))
|
||||
.map(|value| {
|
||||
signals.insert(ReactiveNode {
|
||||
value: Rc::new(RefCell::new(value)),
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Signal,
|
||||
})
|
||||
})
|
||||
.map(|id| {
|
||||
properties.push(ScopeProperty::Signal(id));
|
||||
(
|
||||
@@ -225,6 +480,10 @@ impl RuntimeId {
|
||||
let id = self.create_concrete_signal(
|
||||
Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>
|
||||
);
|
||||
//crate::macros::debug_warn!(
|
||||
// "created RwSignal {id:?} at {:?}",
|
||||
// std::panic::Location::caller()
|
||||
//);
|
||||
RwSignal {
|
||||
runtime: self,
|
||||
id,
|
||||
@@ -237,10 +496,27 @@ impl RuntimeId {
|
||||
#[track_caller]
|
||||
pub(crate) fn create_concrete_effect(
|
||||
self,
|
||||
effect: Rc<dyn AnyEffect>,
|
||||
) -> EffectId {
|
||||
value: Rc<RefCell<dyn Any>>,
|
||||
effect: Rc<dyn AnyComputation>,
|
||||
) -> NodeId {
|
||||
with_runtime(self, |runtime| {
|
||||
runtime.effects.borrow_mut().insert(effect)
|
||||
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value: Rc::clone(&value),
|
||||
state: ReactiveNodeState::Clean,
|
||||
node_type: ReactiveNodeType::Effect {
|
||||
f: Rc::clone(&effect),
|
||||
},
|
||||
});
|
||||
|
||||
// run the effect for the first time
|
||||
let prev_observer = runtime.observer.take();
|
||||
runtime.observer.set(Some(id));
|
||||
|
||||
effect.run(value);
|
||||
|
||||
runtime.observer.set(prev_observer);
|
||||
|
||||
id
|
||||
})
|
||||
.expect("tried to create an effect in a runtime that has been disposed")
|
||||
}
|
||||
@@ -249,7 +525,7 @@ impl RuntimeId {
|
||||
pub(crate) fn create_effect<T>(
|
||||
self,
|
||||
f: impl Fn(Option<T>) -> T + 'static,
|
||||
) -> EffectId
|
||||
) -> NodeId
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
@@ -258,14 +534,13 @@ impl RuntimeId {
|
||||
|
||||
let effect = Effect {
|
||||
f,
|
||||
value: RefCell::new(None),
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
};
|
||||
|
||||
let id = self.create_concrete_effect(Rc::new(effect));
|
||||
id.run(self);
|
||||
id
|
||||
let value = Rc::new(RefCell::new(None::<T>));
|
||||
self.create_concrete_effect(value, Rc::new(effect))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -279,63 +554,31 @@ impl RuntimeId {
|
||||
#[cfg(debug_assertions)]
|
||||
let defined_at = std::panic::Location::caller();
|
||||
|
||||
let (read, write) = self.create_signal(None);
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime.nodes.borrow_mut().insert(ReactiveNode {
|
||||
value: Rc::new(RefCell::new(None::<T>)),
|
||||
// memos are lazy, so are dirty when created
|
||||
// will be run the first time we ask for it
|
||||
state: ReactiveNodeState::Dirty,
|
||||
node_type: ReactiveNodeType::Memo {
|
||||
f: Rc::new(MemoState {
|
||||
f,
|
||||
t: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
.expect("tried to create a memo in a runtime that has been disposed");
|
||||
|
||||
self.create_effect(move |_| {
|
||||
let (new, changed) = read.with_no_subscription(|p| {
|
||||
let new = f(p.as_ref());
|
||||
let changed = Some(&new) != p.as_ref();
|
||||
(new, changed)
|
||||
});
|
||||
|
||||
if changed {
|
||||
write.update(|n| *n = Some(new));
|
||||
}
|
||||
});
|
||||
|
||||
Memo(
|
||||
read,
|
||||
Memo {
|
||||
runtime: self,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Runtime {
|
||||
pub shared_context: RefCell<SharedContext>,
|
||||
pub observer: Cell<Option<EffectId>>,
|
||||
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
|
||||
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
|
||||
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_contexts:
|
||||
RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_cleanups:
|
||||
RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
|
||||
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
|
||||
pub signal_subscribers:
|
||||
RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
|
||||
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
|
||||
pub effect_sources:
|
||||
RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
|
||||
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
|
||||
}
|
||||
|
||||
impl Debug for Runtime {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Runtime")
|
||||
.field("shared_context", &self.shared_context)
|
||||
.field("observer", &self.observer)
|
||||
.field("scopes", &self.scopes)
|
||||
.field("scope_parents", &self.scope_parents)
|
||||
.field("scope_children", &self.scope_children)
|
||||
.field("signals", &self.signals)
|
||||
.field("signal_subscribers", &self.signal_subscribers)
|
||||
.field("effects", &self.effects.borrow().len())
|
||||
.field("effect_sources", &self.effect_sources)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
console_warn,
|
||||
node::NodeId,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
suspense::StreamChunk,
|
||||
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
|
||||
PinnedFuture, ResourceId, StoredValueId, SuspenseContext,
|
||||
};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use std::{collections::HashMap, fmt};
|
||||
@@ -228,17 +229,16 @@ impl Scope {
|
||||
match property {
|
||||
ScopeProperty::Signal(id) => {
|
||||
// remove the signal
|
||||
runtime.signals.borrow_mut().remove(id);
|
||||
runtime.nodes.borrow_mut().remove(id);
|
||||
let subs = runtime
|
||||
.signal_subscribers
|
||||
.node_subscribers
|
||||
.borrow_mut()
|
||||
.remove(id);
|
||||
|
||||
// each of the subs needs to remove the signal from its dependencies
|
||||
// so that it doesn't try to read the (now disposed) signal
|
||||
if let Some(subs) = subs {
|
||||
let source_map =
|
||||
runtime.effect_sources.borrow();
|
||||
let source_map = runtime.node_sources.borrow();
|
||||
for effect in subs.borrow().iter() {
|
||||
if let Some(effect_sources) =
|
||||
source_map.get(*effect)
|
||||
@@ -249,12 +249,15 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
ScopeProperty::Effect(id) => {
|
||||
runtime.effects.borrow_mut().remove(id);
|
||||
runtime.effect_sources.borrow_mut().remove(id);
|
||||
runtime.nodes.borrow_mut().remove(id);
|
||||
runtime.node_sources.borrow_mut().remove(id);
|
||||
}
|
||||
ScopeProperty::Resource(id) => {
|
||||
runtime.resources.borrow_mut().remove(id);
|
||||
}
|
||||
ScopeProperty::StoredValue(id) => {
|
||||
runtime.stored_values.borrow_mut().remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,9 +316,10 @@ slotmap::new_key_type! {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ScopeProperty {
|
||||
Signal(SignalId),
|
||||
Effect(EffectId),
|
||||
Signal(NodeId),
|
||||
Effect(NodeId),
|
||||
Resource(ResourceId),
|
||||
StoredValue(StoredValueId),
|
||||
}
|
||||
|
||||
/// Creating a [Scope](crate::Scope) gives you a disposer, which can be called
|
||||
@@ -438,6 +442,25 @@ impl Scope {
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Batches any reactive updates, preventing effects from running until the whole
|
||||
/// function has run. This allows you to prevent rerunning effects if multiple
|
||||
/// signal updates might cause the same effect to run.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the runtime this scope belongs to has already been disposed.
|
||||
pub fn batch<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
with_runtime(self.runtime, move |runtime| {
|
||||
runtime.batching.set(true);
|
||||
let val = f();
|
||||
runtime.batching.set(false);
|
||||
runtime.run_your_effects();
|
||||
val
|
||||
})
|
||||
.expect(
|
||||
"tried to run a batched update in a runtime that has been disposed",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ScopeDisposer {
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
use crate::{
|
||||
console_warn, create_effect,
|
||||
macros::debug_warn,
|
||||
node::NodeId,
|
||||
on_cleanup,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
Runtime, Scope, ScopeProperty,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::Stream;
|
||||
use std::{fmt::Debug, marker::PhantomData, pin::Pin};
|
||||
use std::{fmt::Debug, marker::PhantomData, pin::Pin, rc::Rc};
|
||||
use thiserror::Error;
|
||||
|
||||
macro_rules! impl_get_fn_traits {
|
||||
@@ -124,6 +125,11 @@ pub trait SignalWith<T> {
|
||||
/// the running effect to this signal. Returns [`Some`] if the signal is
|
||||
/// valid and the function ran, otherwise returns [`None`].
|
||||
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O>;
|
||||
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
fn track(&self) {
|
||||
_ = self.try_with(|_| {});
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait allows setting the value of a signal.
|
||||
@@ -268,6 +274,16 @@ pub trait SignalStream<T> {
|
||||
fn to_stream(&self, cx: Scope) -> Pin<Box<dyn Stream<Item = T>>>;
|
||||
}
|
||||
|
||||
/// This trait allows disposing a signal before its [Scope] has been disposed.
|
||||
pub trait SignalDispose {
|
||||
/// Disposes of the signal. This:
|
||||
/// 1. Detaches the signal from the reactive graph, preventing it from triggering
|
||||
/// further updates; and
|
||||
/// 2. Drops the value contained in the signal.
|
||||
#[track_caller]
|
||||
fn dispose(self);
|
||||
}
|
||||
|
||||
/// Creates a signal, the basic reactive primitive.
|
||||
///
|
||||
/// A signal is a piece of data that may change over time,
|
||||
@@ -469,7 +485,7 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
@@ -719,6 +735,12 @@ impl<T: Clone> SignalStream<T> for ReadSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalDispose for ReadSignal<T> {
|
||||
fn dispose(self) {
|
||||
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadSignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -727,11 +749,6 @@ where
|
||||
self.id.with_no_subscription(self.runtime, f)
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) fn subscribe(&self) {
|
||||
_ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
|
||||
}
|
||||
|
||||
/// Applies the function to the current Signal, if it exists, and subscribes
|
||||
/// the running effect.
|
||||
pub(crate) fn try_with<U>(
|
||||
@@ -812,7 +829,7 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
@@ -1024,6 +1041,12 @@ impl<T> SignalSet<T> for WriteSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalDispose for WriteSignal<T> {
|
||||
fn dispose(self) {
|
||||
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for WriteSignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
@@ -1069,6 +1092,7 @@ impl<T> Copy for WriteSignal<T> {}
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
|
||||
let s = cx.runtime.create_rw_signal(value);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.id)));
|
||||
@@ -1124,7 +1148,7 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) id: NodeId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
@@ -1596,6 +1620,12 @@ impl<T: Clone> SignalStream<T> for RwSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalDispose for RwSignal<T> {
|
||||
fn dispose(self) {
|
||||
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RwSignal<T> {
|
||||
/// Returns a read-only handle to the signal.
|
||||
///
|
||||
@@ -1722,12 +1752,6 @@ impl<T> RwSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// Internals
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a signal.
|
||||
pub struct SignalId;
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum SignalError {
|
||||
#[error("tried to access a signal in a runtime that had been disposed")]
|
||||
@@ -1738,20 +1762,20 @@ pub(crate) enum SignalError {
|
||||
Type(&'static str),
|
||||
}
|
||||
|
||||
impl SignalId {
|
||||
impl NodeId {
|
||||
pub(crate) fn subscribe(&self, runtime: &Runtime) {
|
||||
// add subscriber
|
||||
if let Some(observer) = runtime.observer.get() {
|
||||
// add this observer to the signal's dependencies (to allow notification)
|
||||
let mut subs = runtime.signal_subscribers.borrow_mut();
|
||||
// add this observer to this node's dependencies (to allow notification)
|
||||
let mut subs = runtime.node_subscribers.borrow_mut();
|
||||
if let Some(subs) = subs.entry(*self) {
|
||||
subs.or_default().borrow_mut().insert(observer);
|
||||
}
|
||||
|
||||
// add this signal to the effect's sources (to allow cleanup)
|
||||
let mut effect_sources = runtime.effect_sources.borrow_mut();
|
||||
if let Some(effect_sources) = effect_sources.entry(observer) {
|
||||
let sources = effect_sources.or_default();
|
||||
// add this node to the observer's sources (to allow cleanup)
|
||||
let mut sources = runtime.node_sources.borrow_mut();
|
||||
if let Some(sources) = sources.entry(observer) {
|
||||
let sources = sources.or_default();
|
||||
sources.borrow_mut().insert(*self);
|
||||
}
|
||||
}
|
||||
@@ -1765,29 +1789,18 @@ impl SignalId {
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
// get the value
|
||||
runtime.update_if_necessary(*self);
|
||||
let value = {
|
||||
let signals = runtime.signals.borrow();
|
||||
match signals.get(*self).cloned().ok_or(SignalError::Disposed) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => {
|
||||
debug_warn!("[Signal::try_with] {e}");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}?;
|
||||
let value = value.try_borrow().unwrap_or_else(|e| {
|
||||
debug_warn!(
|
||||
"Signal::try_with_no_subscription failed on Signal<{}>. It \
|
||||
seems you're trying to read the value of a signal within an \
|
||||
effect caused by updating the signal.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
panic!("{e}");
|
||||
});
|
||||
let nodes = runtime.nodes.borrow();
|
||||
let node = nodes.get(*self).ok_or(SignalError::Disposed)?;
|
||||
Rc::clone(&node.value)
|
||||
};
|
||||
|
||||
let value = value.borrow();
|
||||
let value = value
|
||||
.downcast_ref::<T>()
|
||||
.ok_or_else(|| SignalError::Type(std::any::type_name::<T>()))?;
|
||||
.ok_or_else(|| SignalError::Type(std::any::type_name::<T>()))
|
||||
.expect("to downcast signal type");
|
||||
Ok(f(value))
|
||||
}
|
||||
|
||||
@@ -1815,7 +1828,7 @@ impl SignalId {
|
||||
with_runtime(runtime, |runtime| {
|
||||
self.try_with_no_subscription(runtime, f).unwrap()
|
||||
})
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
.expect("runtime to be alive")
|
||||
}
|
||||
|
||||
fn update_value<T, U>(
|
||||
@@ -1828,8 +1841,8 @@ impl SignalId {
|
||||
{
|
||||
with_runtime(runtime, |runtime| {
|
||||
let value = {
|
||||
let signals = runtime.signals.borrow();
|
||||
signals.get(*self).cloned()
|
||||
let signals = runtime.nodes.borrow();
|
||||
signals.get(*self).map(|node| Rc::clone(&node.value))
|
||||
};
|
||||
if let Some(value) = value {
|
||||
let mut value = value.borrow_mut();
|
||||
@@ -1867,27 +1880,40 @@ impl SignalId {
|
||||
T: 'static,
|
||||
{
|
||||
with_runtime(runtime_id, |runtime| {
|
||||
// update the value
|
||||
let updated = self.update_value(runtime_id, f);
|
||||
let value = {
|
||||
let signals = runtime.nodes.borrow();
|
||||
signals.get(*self).map(|node| Rc::clone(&node.value))
|
||||
};
|
||||
let updated = if let Some(value) = value {
|
||||
let mut value = value.borrow_mut();
|
||||
if let Some(value) = value.downcast_mut::<T>() {
|
||||
Some(f(value))
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Signal::update] failed when downcasting to \
|
||||
Signal<{}>",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a Signal<{}> \
|
||||
that has already been disposed of. This is probably \
|
||||
either a logic error in a component that creates and \
|
||||
disposes of scopes, or a Resource resolving after its \
|
||||
scope has been dropped without having been cleaned up.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
None
|
||||
};
|
||||
|
||||
// mark descendants dirty
|
||||
runtime.mark_dirty(*self);
|
||||
|
||||
// notify subscribers
|
||||
if updated.is_some() {
|
||||
let subs = {
|
||||
let subs = runtime.signal_subscribers.borrow();
|
||||
let subs = subs.get(*self);
|
||||
subs.map(|subs| subs.borrow().clone())
|
||||
};
|
||||
if let Some(subs) = subs {
|
||||
for sub in subs {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
effects.get(sub).cloned()
|
||||
};
|
||||
if let Some(effect) = effect {
|
||||
effect.run(sub, runtime_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if updated.is_some() && !runtime.batching.get() {
|
||||
Runtime::run_effects(runtime_id);
|
||||
};
|
||||
updated
|
||||
})
|
||||
|
||||
@@ -22,47 +22,51 @@ use crate::{
|
||||
/// # use leptos_reactive::*;
|
||||
/// # let (cx, disposer) = raw_scope_and_disposer(create_runtime());
|
||||
///
|
||||
/// // this could be serialized to and from localstorage with miniserde
|
||||
/// pub struct State {
|
||||
/// token: String,
|
||||
/// dark_mode: bool,
|
||||
/// // some global state with independent fields
|
||||
/// #[derive(Default, Clone, Debug)]
|
||||
/// struct GlobalState {
|
||||
/// count: u32,
|
||||
/// name: String,
|
||||
/// }
|
||||
///
|
||||
/// let state = create_rw_signal(
|
||||
/// cx,
|
||||
/// State {
|
||||
/// token: "".into(),
|
||||
/// // this would cause flickering on reload,
|
||||
/// // use a cookie for the initial value in real projects
|
||||
/// dark_mode: false,
|
||||
/// },
|
||||
/// );
|
||||
/// let (token, set_token) = create_slice(
|
||||
/// let state = create_rw_signal(cx, GlobalState::default());
|
||||
///
|
||||
/// // `create_slice` lets us create a "lens" into the data
|
||||
/// let (count, set_count) = create_slice(
|
||||
/// cx,
|
||||
/// // we take a slice *from* `state`
|
||||
/// state,
|
||||
/// |state| state.token.clone(),
|
||||
/// |state, value| state.token = value,
|
||||
/// // our getter returns a "slice" of the data
|
||||
/// |state| state.count,
|
||||
/// // our setter describes how to mutate that slice, given a new value
|
||||
/// |state, n| state.count = n,
|
||||
/// );
|
||||
/// let (dark_mode, set_dark_mode) = create_slice(
|
||||
///
|
||||
/// // this slice is completely independent of the `count` slice
|
||||
/// // neither of them will cause the other to rerun
|
||||
/// let (name, set_name) = create_slice(
|
||||
/// cx,
|
||||
/// // we take a slice *from* `state`
|
||||
/// state,
|
||||
/// |state| state.dark_mode,
|
||||
/// |state, value| state.dark_mode = value,
|
||||
/// // our getter returns a "slice" of the data
|
||||
/// |state| state.name.clone(),
|
||||
/// // our setter describes how to mutate that slice, given a new value
|
||||
/// |state, n| state.name = n,
|
||||
/// );
|
||||
/// let count_token_updates = create_rw_signal(cx, 0);
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &0));
|
||||
/// create_isomorphic_effect(cx, move |_| {
|
||||
/// _ = token.with(|_| {});
|
||||
/// count_token_updates.update(|counter| *counter += 1)
|
||||
///
|
||||
/// create_effect(cx, move |_| {
|
||||
/// // note: in the browser, use leptos::log! instead
|
||||
/// println!("name is {}", name());
|
||||
/// });
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &1));
|
||||
/// set_token.set("this is not a token!".into());
|
||||
/// // token was updated with the new token
|
||||
/// token.with(|token| assert_eq!(token, "this is not a token!"));
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &2));
|
||||
/// set_dark_mode.set(true);
|
||||
/// // since token didn't change, there was also no update emitted
|
||||
/// count_token_updates.with(|counter| assert_eq!(counter, &2));
|
||||
/// create_effect(cx, move |_| {
|
||||
/// println!("count is {}", count());
|
||||
/// });
|
||||
///
|
||||
/// // setting count only causes count to log, not name
|
||||
/// set_count(42);
|
||||
///
|
||||
/// // setting name only causes name to log, not count
|
||||
/// set_name("Bob".into());
|
||||
/// ```
|
||||
pub fn create_slice<T, O>(
|
||||
cx: Scope,
|
||||
|
||||
@@ -10,7 +10,7 @@ where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
|
||||
wasm_bindgen_futures::spawn_local(fut)
|
||||
}
|
||||
else if #[cfg(any(test, doctest))] {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
@@ -23,8 +23,7 @@ cfg_if! {
|
||||
} else {
|
||||
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
|
||||
/// in the browser, and simply runs the given function when on the server.
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
pub fn queue_microtask(task: impl FnOnce()) {
|
||||
pub fn queue_microtask(task: impl FnOnce() + 'static) {
|
||||
task();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_rw_signal, RwSignal, Scope, SignalGetUntracked, SignalSetUntracked,
|
||||
SignalUpdateUntracked, SignalWithUntracked,
|
||||
};
|
||||
use crate::{with_runtime, RuntimeId, Scope, ScopeProperty};
|
||||
use std::{cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// Unique ID assigned to a [StoredValue].
|
||||
pub(crate) struct StoredValueId;
|
||||
}
|
||||
|
||||
/// A **non-reactive** wrapper for any value, which can be created with [store_value].
|
||||
///
|
||||
@@ -14,13 +17,22 @@ use crate::{
|
||||
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
|
||||
/// updating it does not notify anything else.
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct StoredValue<T>(RwSignal<T>)
|
||||
pub struct StoredValue<T>
|
||||
where
|
||||
T: 'static;
|
||||
T: 'static,
|
||||
{
|
||||
runtime: RuntimeId,
|
||||
id: StoredValueId,
|
||||
ty: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for StoredValue<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
Self {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: self.ty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +101,7 @@ impl<T> StoredValue<T> {
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.get_untracked()
|
||||
self.try_get_value().expect("could not get stored value")
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::get`] but will not panic by default.
|
||||
@@ -110,7 +122,7 @@ impl<T> StoredValue<T> {
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.try_get_untracked()
|
||||
self.try_with_value(T::clone)
|
||||
}
|
||||
|
||||
/// Applies a function to the current stored value.
|
||||
@@ -163,7 +175,7 @@ impl<T> StoredValue<T> {
|
||||
// track the stored value. This method will also be removed in \
|
||||
// a future version of `leptos`"]
|
||||
pub fn with_value<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
self.0.with_untracked(f)
|
||||
self.try_with_value(f).expect("could not get stored value")
|
||||
}
|
||||
|
||||
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
|
||||
@@ -178,7 +190,15 @@ impl<T> StoredValue<T> {
|
||||
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
|
||||
/// the signal is still valid. [`None`] otherwise.
|
||||
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
self.0.try_with_untracked(f)
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
let value = values.get(self.id)?;
|
||||
let value = value.borrow();
|
||||
let value = value.downcast_ref::<T>()?;
|
||||
Some(f(value))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Updates the stored value.
|
||||
@@ -259,7 +279,8 @@ impl<T> StoredValue<T> {
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn update_value(&self, f: impl FnOnce(&mut T)) {
|
||||
self.0.update_untracked(f);
|
||||
self.try_update_value(f)
|
||||
.expect("could not set stored value");
|
||||
}
|
||||
|
||||
/// Updates the stored value.
|
||||
@@ -277,7 +298,15 @@ impl<T> StoredValue<T> {
|
||||
/// Same as [`Self::update`], but returns [`Some(O)`] if the
|
||||
/// signal is still valid, [`None`] otherwise.
|
||||
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
|
||||
self.0.try_update_untracked(f)
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
let value = values.get(self.id)?;
|
||||
let mut value = value.borrow_mut();
|
||||
let value = value.downcast_mut::<T>()?;
|
||||
Some(f(value))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Sets the stored value.
|
||||
@@ -320,13 +349,26 @@ impl<T> StoredValue<T> {
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn set_value(&self, value: T) {
|
||||
self.0.set_untracked(value);
|
||||
self.try_set_value(value);
|
||||
}
|
||||
|
||||
/// Same as [`Self::set`], but returns [`None`] if the signal is
|
||||
/// still valid, [`Some(T)`] otherwise.
|
||||
pub fn try_set_value(&self, value: T) -> Option<T> {
|
||||
self.0.try_set_untracked(value)
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let values = runtime.stored_values.borrow();
|
||||
let n = values.get(self.id);
|
||||
let mut n = n.map(|n| n.borrow_mut());
|
||||
let n = n.as_mut().and_then(|n| n.downcast_mut::<T>());
|
||||
if let Some(n) = n {
|
||||
*n = value;
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +411,19 @@ pub fn store_value<T>(cx: Scope, value: T) -> StoredValue<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
StoredValue(create_rw_signal(cx, value))
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
runtime
|
||||
.stored_values
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::StoredValue(id)));
|
||||
StoredValue {
|
||||
runtime: cx.runtime,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl_get_fn_traits!(StoredValue(get_value));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(not(feature = "stable"))]
|
||||
use leptos_reactive::{
|
||||
create_isomorphic_effect, create_memo, create_runtime, create_scope,
|
||||
create_signal,
|
||||
create_isomorphic_effect, create_memo, create_runtime, create_rw_signal,
|
||||
create_scope, create_signal, SignalSet,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
@@ -91,3 +91,45 @@ fn untrack_mutes_effect() {
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn batching_actually_batches() {
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let first_name = create_rw_signal(cx, "Greg".to_string());
|
||||
let last_name = create_rw_signal(cx, "Johnston".to_string());
|
||||
|
||||
// simulate an arbitrary side effect
|
||||
let count = Rc::new(Cell::new(0));
|
||||
|
||||
create_isomorphic_effect(cx, {
|
||||
let count = count.clone();
|
||||
move |_| {
|
||||
_ = first_name();
|
||||
_ = last_name();
|
||||
|
||||
count.set(count.get() + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// runs once initially
|
||||
assert_eq!(count.get(), 1);
|
||||
|
||||
// individual updates run effect once each
|
||||
first_name.set("Alice".to_string());
|
||||
assert_eq!(count.get(), 2);
|
||||
|
||||
last_name.set("Smith".to_string());
|
||||
assert_eq!(count.get(), 3);
|
||||
|
||||
// batched effect only runs twice
|
||||
cx.batch(move || {
|
||||
first_name.set("Bob".to_string());
|
||||
last_name.set("Williams".to_string());
|
||||
});
|
||||
assert_eq!(count.get(), 4);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
@@ -33,20 +33,20 @@ fn memo_with_computed_value() {
|
||||
#[test]
|
||||
fn nested_memos() {
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
let c = create_memo(cx, move |_| a() + b());
|
||||
let d = create_memo(cx, move |_| c() * 2);
|
||||
let e = create_memo(cx, move |_| d() + 1);
|
||||
let (a, set_a) = create_signal(cx, 0); // 1
|
||||
let (b, set_b) = create_signal(cx, 0); // 2
|
||||
let c = create_memo(cx, move |_| a() + b()); // 3
|
||||
let d = create_memo(cx, move |_| c() * 2); // 4
|
||||
let e = create_memo(cx, move |_| d() + 1); // 5
|
||||
assert_eq!(d(), 0);
|
||||
set_a(5);
|
||||
assert_eq!(c(), 5);
|
||||
assert_eq!(d(), 10);
|
||||
assert_eq!(e(), 11);
|
||||
assert_eq!(d(), 10);
|
||||
assert_eq!(c(), 5);
|
||||
set_b(1);
|
||||
assert_eq!(c(), 6);
|
||||
assert_eq!(d(), 12);
|
||||
assert_eq!(e(), 13);
|
||||
assert_eq!(d(), 12);
|
||||
assert_eq!(c(), 6);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
@@ -73,7 +73,8 @@ fn memo_runs_only_when_inputs_change() {
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(call_count.get(), 1);
|
||||
// initially the memo has not been called at all, because it's lazy
|
||||
assert_eq!(call_count.get(), 0);
|
||||
|
||||
// here we access the value a bunch of times
|
||||
assert_eq!(c(), 0);
|
||||
@@ -92,3 +93,99 @@ fn memo_runs_only_when_inputs_change() {
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn diamond_problem() {
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (name, set_name) = create_signal(cx, "Greg Johnston".to_string());
|
||||
let first = create_memo(cx, move |_| {
|
||||
name().split_whitespace().next().unwrap().to_string()
|
||||
});
|
||||
let last = create_memo(cx, move |_| {
|
||||
name().split_whitespace().nth(1).unwrap().to_string()
|
||||
});
|
||||
|
||||
let combined_count = Rc::new(Cell::new(0));
|
||||
let combined = create_memo(cx, {
|
||||
let combined_count = Rc::clone(&combined_count);
|
||||
move |_| {
|
||||
combined_count.set(combined_count.get() + 1);
|
||||
format!("{} {}", first(), last())
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(first(), "Greg");
|
||||
assert_eq!(last(), "Johnston");
|
||||
|
||||
set_name("Will Smith".to_string());
|
||||
assert_eq!(first(), "Will");
|
||||
assert_eq!(last(), "Smith");
|
||||
assert_eq!(combined(), "Will Smith");
|
||||
// should not have run the memo logic twice, even
|
||||
// though both paths have been updated
|
||||
assert_eq!(combined_count.get(), 1);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn dynamic_dependencies() {
|
||||
use leptos_reactive::create_isomorphic_effect;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (first, set_first) = create_signal(cx, "Greg");
|
||||
let (last, set_last) = create_signal(cx, "Johnston");
|
||||
let (use_last, set_use_last) = create_signal(cx, true);
|
||||
let name = create_memo(cx, move |_| {
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
first().to_string()
|
||||
}
|
||||
});
|
||||
|
||||
let combined_count = Rc::new(Cell::new(0));
|
||||
|
||||
create_isomorphic_effect(cx, {
|
||||
let combined_count = Rc::clone(&combined_count);
|
||||
move |_| {
|
||||
_ = name();
|
||||
combined_count.set(combined_count.get() + 1);
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(combined_count.get(), 1);
|
||||
|
||||
set_first("Bob");
|
||||
assert_eq!(name(), "Bob Johnston");
|
||||
|
||||
assert_eq!(combined_count.get(), 2);
|
||||
|
||||
set_last("Thompson");
|
||||
|
||||
assert_eq!(combined_count.get(), 3);
|
||||
|
||||
set_use_last(false);
|
||||
|
||||
assert_eq!(name(), "Bob");
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
set_last("Jones");
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
set_last("Smith");
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
set_last("Stevens");
|
||||
assert_eq!(combined_count.get(), 4);
|
||||
|
||||
set_use_last(true);
|
||||
assert_eq!(name(), "Bob Stevens");
|
||||
assert_eq!(combined_count.get(), 5);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
|
||||
57
leptos_reactive/tests/slice.rs
Normal file
57
leptos_reactive/tests/slice.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
#[test]
|
||||
fn slice() {
|
||||
use leptos_reactive::*;
|
||||
let (cx, disposer) = raw_scope_and_disposer(create_runtime());
|
||||
// this could be serialized to and from localstorage with miniserde
|
||||
pub struct State {
|
||||
token: String,
|
||||
dark_mode: bool,
|
||||
}
|
||||
|
||||
let state = create_rw_signal(
|
||||
cx,
|
||||
State {
|
||||
token: "".into(),
|
||||
// this would cause flickering on reload,
|
||||
// use a cookie for the initial value in real projects
|
||||
dark_mode: false,
|
||||
},
|
||||
);
|
||||
|
||||
let (token, set_token) = create_slice(
|
||||
cx,
|
||||
state,
|
||||
|state| state.token.clone(),
|
||||
|state, value| state.token = value,
|
||||
);
|
||||
|
||||
let (_, set_dark_mode) = create_slice(
|
||||
cx,
|
||||
state,
|
||||
|state| state.dark_mode,
|
||||
|state, value| state.dark_mode = value,
|
||||
);
|
||||
|
||||
let count_token_updates = Rc::new(std::cell::Cell::new(0));
|
||||
|
||||
assert_eq!(count_token_updates.get(), 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
let count_token_updates = Rc::clone(&count_token_updates);
|
||||
move |_| {
|
||||
token.track();
|
||||
count_token_updates.set(count_token_updates.get() + 1);
|
||||
}
|
||||
});
|
||||
assert_eq!(count_token_updates.get(), 1);
|
||||
set_token.set("this is not a token!".into());
|
||||
// token was updated with the new token
|
||||
token.with(|token| assert_eq!(token, "this is not a token!"));
|
||||
assert_eq!(count_token_updates.get(), 2);
|
||||
set_dark_mode.set(true);
|
||||
// since token didn't change, there was also no update emitted
|
||||
assert_eq!(count_token_updates.get(), 2);
|
||||
|
||||
disposer.dispose();
|
||||
}
|
||||
@@ -9,38 +9,20 @@ description = "RPC for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn = { workspace = true }
|
||||
lazy_static = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_urlencoded = "0.7"
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
csr = [
|
||||
#"leptos/csr",
|
||||
"leptos_dom/web",
|
||||
"leptos_reactive/csr",
|
||||
]
|
||||
hydrate = [
|
||||
#"leptos/hydrate",
|
||||
"leptos_dom/web",
|
||||
"leptos_reactive/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
#"leptos/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"server_fn/ssr",
|
||||
]
|
||||
stable = [
|
||||
#"leptos/stable",
|
||||
"leptos_dom/stable",
|
||||
"leptos_reactive/stable",
|
||||
]
|
||||
csr = ["leptos_reactive/csr"]
|
||||
hydrate = ["leptos_reactive/hydrate"]
|
||||
ssr = ["leptos_reactive/ssr", "server_fn/ssr"]
|
||||
stable = ["leptos_reactive/stable", "server_fn/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
@@ -286,7 +286,7 @@ where
|
||||
let pending = create_rw_signal(cx, false);
|
||||
let action_fn = Rc::new(move |input: &I| {
|
||||
let fut = action_fn(input);
|
||||
Box::pin(async move { fut.await }) as Pin<Box<dyn Future<Output = O>>>
|
||||
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
|
||||
});
|
||||
|
||||
Action(store_value(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user