mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 12:31:55 -05:00
Compare commits
102 Commits
3704v2
...
0.8.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2bea2e6b7 | ||
|
|
acbd6378a8 | ||
|
|
b7462aab10 | ||
|
|
7593540774 | ||
|
|
ed915f8e06 | ||
|
|
f65d87d566 | ||
|
|
5034539411 | ||
|
|
bc48aa4228 | ||
|
|
d2c81fe955 | ||
|
|
28eb96831a | ||
|
|
599c87c88a | ||
|
|
3ca98279e1 | ||
|
|
a730bffe13 | ||
|
|
18570e970c | ||
|
|
787bf385d3 | ||
|
|
b6d2808671 | ||
|
|
2e4d94b6c6 | ||
|
|
66f9c8c999 | ||
|
|
352080d91a | ||
|
|
1e579614a5 | ||
|
|
fee4bccb32 | ||
|
|
4ba9f67440 | ||
|
|
d84ab6d9bf | ||
|
|
f069d4478e | ||
|
|
65b5d55d62 | ||
|
|
860ad7a221 | ||
|
|
901e038aa0 | ||
|
|
f49f0965bc | ||
|
|
bb62d08d3f | ||
|
|
bfe04593fd | ||
|
|
5b484eaec4 | ||
|
|
5149ad54db | ||
|
|
4d9ec54ad1 | ||
|
|
a1cd7ae9a1 | ||
|
|
3dbb251853 | ||
|
|
98e00fcb3b | ||
|
|
cdee2a9476 | ||
|
|
c97ab9a72c | ||
|
|
4fc8972f2b | ||
|
|
b800c009c7 | ||
|
|
e7a73595de | ||
|
|
a9a988e0e1 | ||
|
|
db10d961df | ||
|
|
fb608158cb | ||
|
|
1a472ebad1 | ||
|
|
2d1b66a5c6 | ||
|
|
c524b0aefc | ||
|
|
e4c977911c | ||
|
|
f488d4b5b7 | ||
|
|
d4cfd0e2cb | ||
|
|
a4b0d3408c | ||
|
|
2037bf12cb | ||
|
|
2747a496fc | ||
|
|
c286812116 | ||
|
|
1e0a9ef189 | ||
|
|
7479010f84 | ||
|
|
9b9983af79 | ||
|
|
04e79a0dc4 | ||
|
|
f64951126e | ||
|
|
0a29071779 | ||
|
|
efcb6f6d21 | ||
|
|
6904eec207 | ||
|
|
7eb8ca702d | ||
|
|
1a7b40b507 | ||
|
|
73dd677843 | ||
|
|
131f414fdc | ||
|
|
c3ed874d4d | ||
|
|
f003e50446 | ||
|
|
ea29685c92 | ||
|
|
49e44a2ec2 | ||
|
|
7157958822 | ||
|
|
d37450d12f | ||
|
|
6ad300c592 | ||
|
|
b4e683d969 | ||
|
|
c2289b23a7 | ||
|
|
299acd25f3 | ||
|
|
287fc47163 | ||
|
|
8f74a6d8a0 | ||
|
|
597175a54b | ||
|
|
ede25b9e3d | ||
|
|
8f636e354a | ||
|
|
7da64f22c4 | ||
|
|
0073ae7d8a | ||
|
|
8465716a19 | ||
|
|
0e24b2e63f | ||
|
|
c64d205984 | ||
|
|
f17cb98eb0 | ||
|
|
30f3e82664 | ||
|
|
152d5a5c92 | ||
|
|
669e1ba7fa | ||
|
|
2ad6a086f9 | ||
|
|
32e58d6b66 | ||
|
|
a107443104 | ||
|
|
c859b07901 | ||
|
|
a9868bea2b | ||
|
|
7183c2b993 | ||
|
|
7a03621db1 | ||
|
|
2b589fa61f | ||
|
|
35e6f17930 | ||
|
|
d1513a4a0b | ||
|
|
aa27b9e474 | ||
|
|
cfe925d58f |
1
.github/workflows/ci-changed-examples.yml
vendored
1
.github/workflows/ci-changed-examples.yml
vendored
@@ -28,5 +28,6 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
|
||||
1
.github/workflows/ci-examples.yml
vendored
1
.github/workflows/ci-examples.yml
vendored
@@ -25,5 +25,6 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -25,5 +25,6 @@ jobs:
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2025-03-05
|
||||
|
||||
@@ -50,5 +50,5 @@ jobs:
|
||||
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create matrix with one item to prevent an empty vector error
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"], \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
2
.github/workflows/get-examples-matrix.yml
vendored
2
.github/workflows/get-examples-matrix.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
sed 's/\/$//' |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":$examples, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
|
||||
2
.github/workflows/get-leptos-matrix.yml
vendored
2
.github/workflows/get-leptos-matrix.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
sed "s|$(pwd)/||" |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Leptos Directories: $crates"
|
||||
echo "matrix={\"directory\":$crates}" >> "$GITHUB_OUTPUT"
|
||||
echo "matrix={\"directory\":$crates, \"erased_mode\": [false, true]}" >> "$GITHUB_OUTPUT"
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
|
||||
6
.github/workflows/run-cargo-make-task.yml
vendored
6
.github/workflows/run-cargo-make-task.yml
vendored
@@ -5,6 +5,9 @@ on:
|
||||
directory:
|
||||
required: true
|
||||
type: string
|
||||
erased_mode:
|
||||
required: true
|
||||
type: boolean
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
@@ -15,9 +18,10 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
RUSTFLAGS: ${{ inputs.erased_mode && '--cfg erase_components' || '' }}
|
||||
jobs:
|
||||
test:
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
name: "Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }}) (erased_mode: ${{ inputs.erased_mode }})"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
|
||||
846
Cargo.lock
generated
846
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@@ -40,38 +40,38 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.7"
|
||||
version = "0.8.0-alpha"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.2.0" }
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.2.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1.5" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.3.0" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.7.7" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.7" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.7" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.7" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.7" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.7" }
|
||||
leptos_router = { path = "./router", version = "0.7.7" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.7" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.7" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.7" }
|
||||
leptos = { path = "./leptos", version = "0.8.0-alpha" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-alpha" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-alpha" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-alpha" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-alpha" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-alpha" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-alpha" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-alpha" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-alpha" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-alpha" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.7" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.7" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.7" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0-alpha" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-alpha" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-alpha" }
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.7.7" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.7" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.7" }
|
||||
tachys = { path = "./tachys", version = "0.1.7" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-alpha" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-alpha" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-alpha" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-alpha" }
|
||||
wasm-bindgen = { version = "0.2.100" }
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -17,11 +17,6 @@ use std::{
|
||||
|
||||
/* Wrapper Types */
|
||||
|
||||
/// This is a result type into which any error can be converted.
|
||||
///
|
||||
/// Results are stored as [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1.0"
|
||||
gloo-utils = "0.2.0"
|
||||
@@ -20,18 +20,27 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
], optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2.92"
|
||||
web-sys = { version = "0.3.69", features = [ "AddEventListenerOptions", "Document", "Element", "Event", "EventListener", "EventTarget", "Performance", "Window" ], optional = true }
|
||||
web-sys = { version = "0.3.69", features = [
|
||||
"AddEventListenerOptions",
|
||||
"Document",
|
||||
"Element",
|
||||
"Event",
|
||||
"EventListener",
|
||||
"EventTarget",
|
||||
"Performance",
|
||||
"Window",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:js-sys",
|
||||
"dep:web-sys",
|
||||
]
|
||||
hydrate = ["leptos/hydrate", "dep:js-sys", "dep:web-sys"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:http-body-util",
|
||||
|
||||
@@ -13,7 +13,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -45,7 +45,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.route("/special/{id}", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::tachys::html::style::style;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -16,7 +15,7 @@ pub enum CatError {
|
||||
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, Error> {
|
||||
if count > 0 {
|
||||
gloo_timers::future::TimeoutFuture::new(1000).await;
|
||||
// make the request
|
||||
@@ -42,11 +41,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
pub fn fetch_example() -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = signal::<CatCount>(1);
|
||||
|
||||
// we use new_unsync here because the reqwasm request type isn't Send
|
||||
// if we were doing SSR, then
|
||||
// 1) we'd want to use a Resource, so the data would be serialized to the client
|
||||
// 2) we'd need to make sure there was a thread-local spawner set up
|
||||
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
|
||||
let cats = LocalResource::new(move || fetch_cats(cat_count.get()));
|
||||
|
||||
let fallback = move |errors: ArcRwSignal<Errors>| {
|
||||
let error_list = move || {
|
||||
@@ -66,8 +61,6 @@ pub fn fetch_example() -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let spreadable = style(("background-color", "AliceBlue"));
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<label>
|
||||
@@ -82,7 +75,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
/>
|
||||
|
||||
</label>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> }>
|
||||
<ErrorBoundary fallback>
|
||||
<ul>
|
||||
{move || Suspend::new(async move {
|
||||
@@ -92,7 +85,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
.map(|s| {
|
||||
view! {
|
||||
<li>
|
||||
<img src=s.clone()/>
|
||||
<img src=s.clone() />
|
||||
</li>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", optional = true, features = ["http2"] }
|
||||
axum = { version = "0.8.1", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
"fs",
|
||||
|
||||
@@ -23,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1.40"
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
axum = { version = "0.7.5", default-features = false, optional = true }
|
||||
axum = { version = "0.8.1", default-features = false, optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
http = { version = "1.1", optional = true }
|
||||
web-sys = { version = "0.3.70", features = [
|
||||
|
||||
@@ -10,15 +10,12 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -10,22 +10,20 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"tracing",
|
||||
"islands",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
"dont-use-islands-router",
|
||||
"islands-router",
|
||||
], optional = true }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen = "0.2.100"
|
||||
serde_json = "1.0.133"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
@@ -58,11 +56,11 @@ site-root = "target/site"
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
style-file = "style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3009"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
|
||||
2852
examples/islands_router/mock_data.json
Normal file
2852
examples/islands_router/mock_data.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,140 +0,0 @@
|
||||
window.addEventListener("click", async (ev) => {
|
||||
// confirm that this is an <a> that meets our requirements
|
||||
if (
|
||||
ev.defaultPrevented ||
|
||||
ev.button !== 0 ||
|
||||
ev.metaKey ||
|
||||
ev.altKey ||
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey
|
||||
)
|
||||
return;
|
||||
|
||||
/** @type HTMLAnchorElement | undefined;*/
|
||||
const a = ev
|
||||
.composedPath()
|
||||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
||||
|
||||
if (!a) return;
|
||||
|
||||
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
const target = svg ? a.target.baseVal : a.target;
|
||||
if (target || (!href && !a.hasAttribute("state"))) return;
|
||||
|
||||
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
||||
if (a.hasAttribute("download") || (rel && rel.includes("external"))) return;
|
||||
|
||||
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
||||
if (
|
||||
url.origin !== window.location.origin // ||
|
||||
// TODO base
|
||||
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
|
||||
)
|
||||
return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// fetch the new page
|
||||
const resp = await fetch(url);
|
||||
const htmlString = await resp.text();
|
||||
|
||||
// Use DOMParser to parse the HTML string
|
||||
const parser = new DOMParser();
|
||||
// TODO parse from the request stream instead?
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = async () => {
|
||||
const oldDocWalker = document.createTreeWalker(document);
|
||||
const newDocWalker = doc.createTreeWalker(doc);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
let newNode = newDocWalker.currentNode;
|
||||
while(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
oldNode = oldDocWalker.currentNode;
|
||||
newNode = newDocWalker.currentNode;
|
||||
// if the nodes are different, we need to replace the old with the new
|
||||
// because of the typed view tree, this should never actually happen
|
||||
if (oldNode.nodeType !== newNode.nodeType) {
|
||||
oldNode.replaceWith(newNode);
|
||||
}
|
||||
// if it's a text node, just update the text with the new text
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
/** @type Element */
|
||||
const oldEl = oldNode;
|
||||
/** @type Element */
|
||||
const newEl = newNode;
|
||||
if (oldEl.tagName !== newEl.tagName) {
|
||||
oldEl.replaceWith(newEl);
|
||||
}
|
||||
else {
|
||||
for(const attr of newEl.attributes) {
|
||||
oldEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
|
||||
// if one of these marker is hit, then there are two options
|
||||
// 1) it's the same branch, and we just keep walking until the end
|
||||
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
|
||||
else if (oldNode.nodeType === Node.COMMENT_NODE) {
|
||||
const oldText = oldNode.textContent;
|
||||
const newText = newNode.textContent;
|
||||
if(oldText.startsWith("bo") && newText !== oldText) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
while(oldBranches > 0 && newBranches > 0) {
|
||||
if(oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
console.log(oldDocWalker.currentNode, newDocWalker.currentNode);
|
||||
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
oldBranches += 1;
|
||||
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
}
|
||||
}
|
||||
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
newBranches += 1;
|
||||
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
newBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndBefore(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} }
|
||||
}
|
||||
};
|
||||
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
|
||||
if (document.startViewTransition) {
|
||||
await document.startViewTransition(transition);
|
||||
} else {
|
||||
await transition()
|
||||
}
|
||||
window.history.pushState(undefined, null, url);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
StaticSegment,
|
||||
use leptos::{
|
||||
either::{Either, EitherOf3},
|
||||
prelude::*,
|
||||
};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
hooks::{use_params_map, use_query_map},
|
||||
path,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
@@ -12,7 +17,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options=options islands=true/>
|
||||
<HydrationScripts options=options islands=true islands_router=true/>
|
||||
<link rel="stylesheet" id="leptos" href="/pkg/islands.css"/>
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
|
||||
</head>
|
||||
@@ -26,34 +31,180 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
<script src="/routing.js"></script>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Application"</h1>
|
||||
<h1>"My Contacts"</h1>
|
||||
</header>
|
||||
<nav>
|
||||
<a href="/">"Page A"</a>
|
||||
<a href="/b">"Page B"</a>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/about">"About"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<p>
|
||||
<label>"Home Checkbox" <input type="checkbox"/></label>
|
||||
</p>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=StaticSegment("") view=PageA/>
|
||||
<Route path=StaticSegment("b") view=PageB/>
|
||||
</FlatRoutes>
|
||||
<Routes fallback=|| "Not found.">
|
||||
<Route path=path!("") view=Home/>
|
||||
<Route path=path!("user/:id") view=Details/>
|
||||
<Route path=path!("about") view=About/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PageA() -> impl IntoView {
|
||||
view! { <label>"Page A" <input type="checkbox"/></label> }
|
||||
#[server]
|
||||
pub async fn search(query: String) -> Result<Vec<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
let query = query.to_ascii_lowercase();
|
||||
Ok(data
|
||||
.into_iter()
|
||||
.filter(|user| {
|
||||
user.first_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.last_name.to_ascii_lowercase().contains(&query)
|
||||
|| user.email.to_ascii_lowercase().contains(&query)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn delete_user(id: u32) -> Result<(), ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let mut data: Vec<User> = serde_json::from_str(&users)?;
|
||||
data.retain(|user| user.id != id);
|
||||
let new_json = serde_json::to_string(&data)?;
|
||||
tokio::fs::write("./mock_data.json", &new_json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct User {
|
||||
id: u32,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PageB() -> impl IntoView {
|
||||
view! { <label>"Page B" <input type="checkbox"/></label> }
|
||||
pub fn Home() -> impl IntoView {
|
||||
let q = use_query_map();
|
||||
let q = move || q.read().get("q");
|
||||
let data = Resource::new(q, |q| async move {
|
||||
if let Some(q) = q {
|
||||
search(q).await
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
});
|
||||
let delete_user_action = ServerAction::<DeleteUser>::new();
|
||||
|
||||
let view = move || {
|
||||
Suspend::new(async move {
|
||||
let users = data.await.unwrap();
|
||||
if q().is_none() {
|
||||
EitherOf3::A(view! {
|
||||
<p class="note">"Enter a search to begin viewing contacts."</p>
|
||||
})
|
||||
} else if users.is_empty() {
|
||||
EitherOf3::B(view! {
|
||||
<p class="note">"No users found matching that search."</p>
|
||||
})
|
||||
} else {
|
||||
EitherOf3::C(view! {
|
||||
<table>
|
||||
<tbody>
|
||||
<For
|
||||
each=move || users.clone()
|
||||
key=|user| user.id
|
||||
let:user
|
||||
>
|
||||
<tr>
|
||||
<td>{user.first_name}</td>
|
||||
<td>{user.last_name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
<a href=format!("/user/{}", user.id)>"Details"</a>
|
||||
<input type="checkbox"/>
|
||||
<ActionForm action=delete_user_action>
|
||||
<input type="hidden" name="id" value=user.id/>
|
||||
<input type="submit" value="Delete"/>
|
||||
</ActionForm>
|
||||
</td>
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
view! {
|
||||
<section class="page">
|
||||
<form method="GET" class="search">
|
||||
<input type="search" name="q" value=q autofocus oninput="this.form.requestSubmit()"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
<Suspense fallback=|| view! { <p>"Loading..."</p> }>{view}</Suspense>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Details() -> impl IntoView {
|
||||
#[server]
|
||||
pub async fn get_user(id: u32) -> Result<Option<User>, ServerFnError> {
|
||||
let users = tokio::fs::read_to_string("./mock_data.json").await?;
|
||||
let data: Vec<User> = serde_json::from_str(&users)?;
|
||||
Ok(data.iter().find(|user| user.id == id).cloned())
|
||||
}
|
||||
let params = use_params_map();
|
||||
let id = move || {
|
||||
params
|
||||
.read()
|
||||
.get("id")
|
||||
.and_then(|id| id.parse::<u32>().ok())
|
||||
};
|
||||
let user = Resource::new(id, |id| async move {
|
||||
match id {
|
||||
None => Ok(None),
|
||||
Some(id) => get_user(id).await,
|
||||
}
|
||||
});
|
||||
|
||||
move || {
|
||||
Suspend::new(async move {
|
||||
user.await.map(|user| match user {
|
||||
None => Either::Left(view! {
|
||||
<section class="page">
|
||||
<h2>"Not found."</h2>
|
||||
<p>"Sorry — we couldn’t find that user."</p>
|
||||
</section>
|
||||
}),
|
||||
Some(user) => Either::Right(view! {
|
||||
<section class="page">
|
||||
<h2>{user.first_name} " " { user.last_name}</h2>
|
||||
<p class="email">{user.email}</p>
|
||||
</section>
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn About() -> impl IntoView {
|
||||
view! {
|
||||
<section class="page">
|
||||
<h2>"About"</h2>
|
||||
<p>"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."</p>
|
||||
<p>"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"</p>
|
||||
<Counter/>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let count = RwSignal::new(0);
|
||||
view! {
|
||||
<button class="counter" on:click=move |_| *count.write() += 1>{count}</button>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,52 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: #f6f6fa;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, source-sans-pro, sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
form.search {
|
||||
display: flex;
|
||||
margin: 2rem auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
td {
|
||||
min-width: 10rem;
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
td:last-child > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note, .note {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.counter {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -149,12 +149,12 @@ pub fn App() -> impl IntoView {
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<Button id="run" text="Create 1,000 rows" on:click=run/>
|
||||
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots/>
|
||||
<Button id="add" text="Append 1,000 rows" on:click=add/>
|
||||
<Button id="update" text="Update every 10th row" on:click=update/>
|
||||
<Button id="clear" text="Clear" on:click=clear/>
|
||||
<Button id="swaprows" text="Swap Rows" on:click=swap_rows/>
|
||||
<Button id="run" text="Create 1,000 rows" on:click=run />
|
||||
<Button id="runlots" text="Create 10,000 rows" on:click=run_lots />
|
||||
<Button id="add" text="Append 1,000 rows" on:click=add />
|
||||
<Button id="update" text="Update every 10th row" on:click=update />
|
||||
<Button id="clear" text="Clear" on:click=clear />
|
||||
<Button id="swaprows" text="Swap Rows" on:click=swap_rows />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@ use leptos_router::{
|
||||
},
|
||||
hooks::{use_navigate, use_params, use_query_map},
|
||||
params::Params,
|
||||
MatchNestedRoutes,
|
||||
};
|
||||
use leptos_router_macro::path;
|
||||
use std::time::Duration;
|
||||
@@ -33,7 +32,7 @@ pub fn RouterExample() -> impl IntoView {
|
||||
<Router set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<div class="routing-progress">
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250) />
|
||||
</div>
|
||||
<nav>
|
||||
// ordinary <a> elements can be used for client-side navigation
|
||||
@@ -53,15 +52,15 @@ pub fn RouterExample() -> impl IntoView {
|
||||
<Routes transition=true fallback=|| "This page could not be found.">
|
||||
// paths can be created using the path!() macro, or provided as types like
|
||||
// StaticSegment("about")
|
||||
<Route path=path!("about") view=About/>
|
||||
<Route path=path!("about") view=About />
|
||||
<ProtectedRoute
|
||||
path=path!("settings")
|
||||
condition=move || Some(logged_in.get())
|
||||
redirect_path=|| "/"
|
||||
view=Settings
|
||||
/>
|
||||
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/"/> }/>
|
||||
<ContactRoutes/>
|
||||
<Route path=path!("redirect-home") view=|| view! { <Redirect path="/" /> } />
|
||||
<ContactRoutes />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -71,11 +70,11 @@ pub fn RouterExample() -> impl IntoView {
|
||||
// You can define other routes in their own component.
|
||||
// Routes implement the MatchNestedRoutes
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
pub fn ContactRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("") view=ContactList>
|
||||
<Route path=path!("/") view=|| "Select a contact."/>
|
||||
<Route path=path!("/:id") view=Contact/>
|
||||
<Route path=path!("/") view=|| "Select a contact." />
|
||||
<Route path=path!("/:id") view=Contact />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
@@ -122,7 +121,7 @@ pub fn ContactList() -> impl IntoView {
|
||||
<Suspense fallback=move || view! { <p>"Loading contacts..."</p> }>
|
||||
<ul>{contacts}</ul>
|
||||
</Suspense>
|
||||
<Outlet/>
|
||||
<Outlet />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -166,7 +165,7 @@ pub fn Contact() -> impl IntoView {
|
||||
Some(contact) => Either::Right(view! {
|
||||
<section class="card">
|
||||
<h1>{contact.first_name} " " {contact.last_name}</h1>
|
||||
<p>{contact.address_1} <br/> {contact.address_2}</p>
|
||||
<p>{contact.address_1} <br /> {contact.address_2}</p>
|
||||
</section>
|
||||
}),
|
||||
}
|
||||
@@ -224,10 +223,10 @@ pub fn Settings() -> impl IntoView {
|
||||
<Form action="">
|
||||
<fieldset>
|
||||
<legend>"Name"</legend>
|
||||
<input type="text" name="first_name" placeholder="First"/>
|
||||
<input type="text" name="last_name" placeholder="Last"/>
|
||||
<input type="text" name="first_name" placeholder="First" />
|
||||
<input type="text" name="last_name" placeholder="Last" />
|
||||
</fieldset>
|
||||
<input type="submit"/>
|
||||
<input type="submit" />
|
||||
<p>
|
||||
"This uses the " <code>"<Form/>"</code>
|
||||
" component, which enhances forms by using client-side navigation for "
|
||||
|
||||
@@ -21,7 +21,7 @@ server_fn = { path = "../../server_fn", features = [
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.5.2", optional = true }
|
||||
tower-http = { version = "0.6.2", features = [
|
||||
"fs",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use futures::StreamExt;
|
||||
use futures::{Sink, Stream, StreamExt};
|
||||
use http::Method;
|
||||
use leptos::{html::Input, prelude::*, task::spawn_local};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
@@ -9,8 +9,10 @@ use server_fn::{
|
||||
MultipartFormData, Postcard, Rkyv, SerdeLite, StreamingText,
|
||||
TextStream,
|
||||
},
|
||||
error::{FromServerFnError, IntoAppError, ServerFnErrorErr},
|
||||
request::{browser::BrowserRequest, ClientReq, Req},
|
||||
response::{browser::BrowserResponse, ClientRes, Res},
|
||||
response::{browser::BrowserResponse, ClientRes, TryRes},
|
||||
ContentType,
|
||||
};
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -652,32 +654,72 @@ pub fn FileWatcher() -> impl IntoView {
|
||||
/// implementations if you'd like. However, it's much lighter weight to use something like `strum`
|
||||
/// simply to generate those trait implementations.
|
||||
#[server]
|
||||
pub async fn ascii_uppercase(
|
||||
text: String,
|
||||
) -> Result<String, ServerFnError<InvalidArgument>> {
|
||||
pub async fn ascii_uppercase(text: String) -> Result<String, MyErrors> {
|
||||
other_error()?;
|
||||
Ok(ascii_uppercase_inner(text)?)
|
||||
}
|
||||
|
||||
pub fn other_error() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ascii_uppercase_inner(text: String) -> Result<String, InvalidArgument> {
|
||||
if text.len() < 5 {
|
||||
Err(InvalidArgument::TooShort.into())
|
||||
Err(InvalidArgument::TooShort)
|
||||
} else if text.len() > 15 {
|
||||
Err(InvalidArgument::TooLong.into())
|
||||
Err(InvalidArgument::TooLong)
|
||||
} else if text.is_ascii() {
|
||||
Ok(text.to_ascii_uppercase())
|
||||
} else {
|
||||
Err(InvalidArgument::NotAscii.into())
|
||||
Err(InvalidArgument::NotAscii)
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn ascii_uppercase_classic(
|
||||
text: String,
|
||||
) -> Result<String, ServerFnError<InvalidArgument>> {
|
||||
Ok(ascii_uppercase_inner(text)?)
|
||||
}
|
||||
|
||||
// The EnumString and Display derive macros are provided by strum
|
||||
#[derive(Debug, Clone, EnumString, Display)]
|
||||
#[derive(Debug, Clone, Display, EnumString, Serialize, Deserialize)]
|
||||
pub enum InvalidArgument {
|
||||
TooShort,
|
||||
TooLong,
|
||||
NotAscii,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Serialize, Deserialize)]
|
||||
pub enum MyErrors {
|
||||
InvalidArgument(InvalidArgument),
|
||||
ServerFnError(ServerFnErrorErr),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<InvalidArgument> for MyErrors {
|
||||
fn from(value: InvalidArgument) -> Self {
|
||||
MyErrors::InvalidArgument(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for MyErrors {
|
||||
fn from(value: String) -> Self {
|
||||
MyErrors::Other(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromServerFnError for MyErrors {
|
||||
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
|
||||
MyErrors::ServerFnError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CustomErrorTypes() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (result, set_result) = signal(None);
|
||||
let (result_classic, set_result_classic) = signal(None);
|
||||
|
||||
view! {
|
||||
<h3>Using custom error types</h3>
|
||||
@@ -692,14 +734,17 @@ pub fn CustomErrorTypes() -> impl IntoView {
|
||||
<button on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
spawn_local(async move {
|
||||
let data = ascii_uppercase(value).await;
|
||||
let data = ascii_uppercase(value.clone()).await;
|
||||
let data_classic = ascii_uppercase_classic(value).await;
|
||||
set_result.set(Some(data));
|
||||
set_result_classic.set(Some(data_classic));
|
||||
});
|
||||
}>
|
||||
|
||||
"Submit"
|
||||
</button>
|
||||
<p>{move || format!("{:?}", result.get())}</p>
|
||||
<p>{move || format!("{:?}", result_classic.get())}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,8 +762,11 @@ pub struct Toml;
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TomlEncoded<T>(T);
|
||||
|
||||
impl Encoding for Toml {
|
||||
impl ContentType for Toml {
|
||||
const CONTENT_TYPE: &'static str = "application/toml";
|
||||
}
|
||||
|
||||
impl Encoding for Toml {
|
||||
const METHOD: Method = Method::POST;
|
||||
}
|
||||
|
||||
@@ -726,14 +774,12 @@ impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: ClientReq<Err>,
|
||||
T: Serialize,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
fn into_req(
|
||||
self,
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
) -> Result<Request, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
|
||||
let data = toml::to_string(&self.0).map_err(|e| {
|
||||
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
|
||||
})?;
|
||||
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
|
||||
}
|
||||
}
|
||||
@@ -742,23 +788,26 @@ impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: Req<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
|
||||
async fn from_req(req: Request) -> Result<Self, Err> {
|
||||
let string_data = req.try_into_string().await?;
|
||||
toml::from_str::<T>(&string_data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: Res<Err>,
|
||||
Response: TryRes<Err>,
|
||||
T: Serialize + Send,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
async fn into_res(self) -> Result<Response, Err> {
|
||||
let data = toml::to_string(&self.0).map_err(|e| {
|
||||
ServerFnErrorErr::Serialization(e.to_string()).into_app_error()
|
||||
})?;
|
||||
Response::try_from_string(Toml::CONTENT_TYPE, data)
|
||||
}
|
||||
}
|
||||
@@ -767,12 +816,13 @@ impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: ClientRes<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
Err: FromServerFnError,
|
||||
{
|
||||
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
|
||||
async fn from_res(res: Response) -> Result<Self, Err> {
|
||||
let data = res.try_into_string().await?;
|
||||
toml::from_str(&data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
toml::from_str(&data).map(TomlEncoded).map_err(|e| {
|
||||
ServerFnErrorErr::Deserialization(e.to_string()).into_app_error()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,7 +885,10 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
pub struct CustomClient;
|
||||
|
||||
// Implement the `Client` trait for it.
|
||||
impl<CustErr> Client<CustErr> for CustomClient {
|
||||
impl<E> Client<E> for CustomClient
|
||||
where
|
||||
E: FromServerFnError,
|
||||
{
|
||||
// BrowserRequest and BrowserResponse are the defaults used by other server functions.
|
||||
// They are wrappers for the underlying Web Fetch API types.
|
||||
type Request = BrowserRequest;
|
||||
@@ -844,8 +897,7 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
// Our custom `send()` implementation does all the work.
|
||||
fn send(
|
||||
req: Self::Request,
|
||||
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
|
||||
+ Send {
|
||||
) -> impl Future<Output = Result<Self::Response, E>> + Send {
|
||||
// BrowserRequest derefs to the underlying Request type from gloo-net,
|
||||
// so we can get access to the headers here
|
||||
let headers = req.headers();
|
||||
@@ -854,6 +906,24 @@ pub fn CustomClientExample() -> impl IntoView {
|
||||
// delegate back out to BrowserClient to send the modified request
|
||||
BrowserClient::send(req)
|
||||
}
|
||||
|
||||
fn open_websocket(
|
||||
path: &str,
|
||||
) -> impl Future<
|
||||
Output = Result<
|
||||
(
|
||||
impl Stream<Item = Result<server_fn::Bytes, E>> + Send + 'static,
|
||||
impl Sink<Result<server_fn::Bytes, E>> + Send + 'static,
|
||||
),
|
||||
E,
|
||||
>,
|
||||
> + Send {
|
||||
BrowserClient::open_websocket(path)
|
||||
}
|
||||
|
||||
fn spawn(future: impl Future<Output = ()> + Send + 'static) {
|
||||
<BrowserClient as Client<E>>::spawn(future)
|
||||
}
|
||||
}
|
||||
|
||||
// Specify our custom client with `client = `
|
||||
|
||||
@@ -20,7 +20,7 @@ leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
|
||||
@@ -18,7 +18,7 @@ leptos_router = { path = "../../router" }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = [
|
||||
@@ -45,7 +45,7 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
"leptos_router/ssr",
|
||||
"dep:notify",
|
||||
"dep:http"
|
||||
"dep:http",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -4,7 +4,7 @@ use leptos_router::{
|
||||
hooks::use_params,
|
||||
nested_router::Outlet,
|
||||
params::Params,
|
||||
MatchNestedRoutes, ParamSegment, SsrMode, StaticSegment, WildcardSegment,
|
||||
ParamSegment, SsrMode, StaticSegment, WildcardSegment,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -21,6 +21,7 @@ pub(super) mod counter {
|
||||
pub struct Counter(AtomicU32);
|
||||
|
||||
impl Counter {
|
||||
#[allow(dead_code)]
|
||||
pub const fn new() -> Self {
|
||||
Self(AtomicU32::new(0))
|
||||
}
|
||||
@@ -203,20 +204,20 @@ pub struct SuspenseCounters {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn InstrumentedRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
pub fn InstrumentedRoutes() -> impl leptos_router::MatchNestedRoutes + Clone {
|
||||
// TODO should make this mode configurable via feature flag?
|
||||
let ssr = SsrMode::Async;
|
||||
view! {
|
||||
<ParentRoute path=StaticSegment("instrumented") view=InstrumentedRoot ssr>
|
||||
<Route path=StaticSegment("/") view=InstrumentedTop/>
|
||||
<Route path=StaticSegment("/") view=InstrumentedTop />
|
||||
<ParentRoute path=StaticSegment("item") view=ItemRoot>
|
||||
<Route path=StaticSegment("/") view=ItemListing/>
|
||||
<Route path=StaticSegment("/") view=ItemListing />
|
||||
<ParentRoute path=ParamSegment("id") view=ItemTop>
|
||||
<Route path=StaticSegment("/") view=ItemOverview/>
|
||||
<Route path=WildcardSegment("path") view=ItemInspect/>
|
||||
<Route path=StaticSegment("/") view=ItemOverview />
|
||||
<Route path=WildcardSegment("path") view=ItemInspect />
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
<Route path=StaticSegment("counters") view=ShowCounters/>
|
||||
<Route path=StaticSegment("counters") view=ShowCounters />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
@@ -279,32 +280,41 @@ fn InstrumentedRoot() -> impl IntoView {
|
||||
<section id="instrumented">
|
||||
<nav>
|
||||
<a href="/">"Site Root"</a>
|
||||
<A href="./" exact=true>"Instrumented Root"</A>
|
||||
<A href="item/" strict_trailing_slash=true>"Item Listing"</A>
|
||||
<A href="counters" strict_trailing_slash=true>"Counters"</A>
|
||||
<A href="./" exact=true>
|
||||
"Instrumented Root"
|
||||
</A>
|
||||
<A href="item/" strict_trailing_slash=true>
|
||||
"Item Listing"
|
||||
</A>
|
||||
<A href="counters" strict_trailing_slash=true>
|
||||
"Counters"
|
||||
</A>
|
||||
</nav>
|
||||
<FieldNavPortlet/>
|
||||
<Outlet/>
|
||||
<Suspense>{
|
||||
move || Suspend::new(async move {
|
||||
<FieldNavPortlet />
|
||||
<Outlet />
|
||||
<Suspense>
|
||||
{move || Suspend::new(async move {
|
||||
let clear_suspense_counters = move |_| {
|
||||
counters.update(|c| *c = SuspenseCounters::default());
|
||||
};
|
||||
csr_ticket.get().map(|ticket| {
|
||||
let ticket = ticket.0;
|
||||
view! {
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-csr-counters"
|
||||
type="submit"
|
||||
value="Reset CSR Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})
|
||||
}</Suspense>
|
||||
csr_ticket
|
||||
.get()
|
||||
.map(|ticket| {
|
||||
let ticket = ticket.0;
|
||||
view! {
|
||||
<ActionForm action=reset_counters>
|
||||
<input type="hidden" name="ticket" value=format!("{ticket}") />
|
||||
<input
|
||||
id="reset-csr-counters"
|
||||
type="submit"
|
||||
value="Reset CSR Counters"
|
||||
on:click=clear_suspense_counters
|
||||
/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
})}
|
||||
</Suspense>
|
||||
<footer>
|
||||
<nav>
|
||||
<A href="item/3/">"Target 3##"</A>
|
||||
@@ -323,11 +333,17 @@ fn InstrumentedRoot() -> impl IntoView {
|
||||
fn InstrumentedTop() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Instrumented Tests"</h1>
|
||||
<p>"These tests validates the number of invocations of server functions and suspenses per access."</p>
|
||||
<p>
|
||||
"These tests validates the number of invocations of server functions and suspenses per access."
|
||||
</p>
|
||||
<ul>
|
||||
// not using `A` because currently some bugs with artix
|
||||
<li><a href="item/">"Item Listing"</a></li>
|
||||
<li><a href="item/4/path1/">"Target 41#"</a></li>
|
||||
<li>
|
||||
<a href="item/">"Item Listing"</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="item/4/path1/">"Target 41#"</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
@@ -342,7 +358,7 @@ fn ItemRoot() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h2>"<ItemRoot/>"</h2>
|
||||
<Outlet/>
|
||||
<Outlet />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +376,9 @@ fn ItemListing() -> impl IntoView {
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{item}/")>"Item "{item}</A></li>
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{item}/")>"Item "{item}</a></li>
|
||||
<li>
|
||||
<a href=format!("/instrumented/item/{item}/")>"Item "{item}</a>
|
||||
</li>
|
||||
}
|
||||
)
|
||||
.collect_view()
|
||||
@@ -373,9 +391,7 @@ fn ItemListing() -> impl IntoView {
|
||||
view! {
|
||||
<h3>"<ItemListing/>"</h3>
|
||||
<ul>
|
||||
<Suspense>
|
||||
{item_listing}
|
||||
</Suspense>
|
||||
<Suspense>{item_listing}</Suspense>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
@@ -402,7 +418,7 @@ fn ItemTop() -> impl IntoView {
|
||||
));
|
||||
view! {
|
||||
<h4>"<ItemTop/>"</h4>
|
||||
<Outlet/>
|
||||
<Outlet />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,24 +428,29 @@ fn ItemOverview() -> impl IntoView {
|
||||
let resource = expect_context::<Resource<Option<GetItemResult>>>();
|
||||
let item_view = move || {
|
||||
Suspend::new(async move {
|
||||
let result = resource.await.map(|GetItemResult(item, names)| view! {
|
||||
<p>{format!("Viewing {item:?}")}</p>
|
||||
<ul>{
|
||||
names.into_iter()
|
||||
.map(|name| {
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
|
||||
let id = item.id;
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/")>
|
||||
"Inspect "{name.clone()}
|
||||
</a></li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
});
|
||||
let result = resource.await.map(|GetItemResult(item, names)| {
|
||||
view! {
|
||||
<p>{format!("Viewing {item:?}")}</p>
|
||||
<ul>
|
||||
{names
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let id = item.id;
|
||||
// FIXME seems like relative link isn't working, it is currently
|
||||
// adding an extra `/` in artix; manually construct `a` instead.
|
||||
// <li><A href=format!("./{name}/")>{format!("Inspect {name}")}</A></li>
|
||||
view! {
|
||||
<li>
|
||||
<a href=format!(
|
||||
"/instrumented/item/{id}/{name}/",
|
||||
)>"Inspect "{name.clone()}</a>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
</ul>
|
||||
}
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_overview += 1);
|
||||
result
|
||||
})
|
||||
@@ -437,9 +458,7 @@ fn ItemOverview() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h5>"<ItemOverview/>"</h5>
|
||||
<Suspense>
|
||||
{item_view}
|
||||
</Suspense>
|
||||
<Suspense>{item_view}</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,23 +515,26 @@ fn ItemInspect() -> impl IntoView {
|
||||
));
|
||||
view! {
|
||||
<p>{format!("Inspecting {item:?}")}</p>
|
||||
<ul>{
|
||||
fields.iter()
|
||||
<ul>
|
||||
{fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
// FIXME seems like relative link to root for a wildcard isn't
|
||||
// working as expected, so manually construct `a` instead.
|
||||
// let text = format!("Inspect {name}/{field}");
|
||||
// view! {
|
||||
// <li><A href=format!("{field}")>{text}</A></li>
|
||||
// <li><A href=format!("{field}")>{text}</A></li>
|
||||
// }
|
||||
view! {
|
||||
<li><a href=format!("/instrumented/item/{id}/{name}/{field}")>{
|
||||
format!("Inspect {name}/{field}")
|
||||
}</a></li>
|
||||
<li>
|
||||
<a href=format!(
|
||||
"/instrumented/item/{id}/{name}/{field}",
|
||||
)>{format!("Inspect {name}/{field}")}</a>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}</ul>
|
||||
.collect_view()}
|
||||
</ul>
|
||||
}
|
||||
});
|
||||
suspense_counters.update_untracked(|c| c.item_inspect += 1);
|
||||
@@ -527,9 +549,7 @@ fn ItemInspect() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h5>"<ItemInspect/>"</h5>
|
||||
<Suspense>
|
||||
{inspect_view}
|
||||
</Suspense>
|
||||
<Suspense>{inspect_view}</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +610,8 @@ fn ShowCounters() -> impl IntoView {
|
||||
id="reset-counters"
|
||||
type="submit"
|
||||
value="Reset Counters"
|
||||
on:click=clear_suspense_counters/>
|
||||
on:click=clear_suspense_counters
|
||||
/>
|
||||
</ActionForm>
|
||||
}
|
||||
})
|
||||
@@ -601,20 +622,23 @@ fn ShowCounters() -> impl IntoView {
|
||||
<h2>"Counters"</h2>
|
||||
|
||||
<h3 id="suspend-calls">"Suspend Calls"</h3>
|
||||
{move || suspense_counters.with(|c| view! {
|
||||
<dl>
|
||||
<dt>"item_listing"</dt>
|
||||
<dd id="item_listing">{c.item_listing}</dd>
|
||||
<dt>"item_overview"</dt>
|
||||
<dd id="item_overview">{c.item_overview}</dd>
|
||||
<dt>"item_inspect"</dt>
|
||||
<dd id="item_inspect">{c.item_inspect}</dd>
|
||||
</dl>
|
||||
})}
|
||||
{move || {
|
||||
suspense_counters
|
||||
.with(|c| {
|
||||
view! {
|
||||
<dl>
|
||||
<dt>"item_listing"</dt>
|
||||
<dd id="item_listing">{c.item_listing}</dd>
|
||||
<dt>"item_overview"</dt>
|
||||
<dd id="item_overview">{c.item_overview}</dd>
|
||||
<dt>"item_inspect"</dt>
|
||||
<dd id="item_inspect">{c.item_inspect}</dd>
|
||||
</dl>
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
<Suspense>
|
||||
{counter_view}
|
||||
</Suspense>
|
||||
<Suspense>{counter_view}</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,17 +666,17 @@ pub fn FieldNavPortlet() -> impl IntoView {
|
||||
view! {
|
||||
<div id="FieldNavPortlet">
|
||||
<span>"FieldNavPortlet:"</span>
|
||||
<nav>{
|
||||
ctx.0.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! {
|
||||
<A href=href>{text}</A>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
})
|
||||
}</nav>
|
||||
<nav>
|
||||
{ctx
|
||||
.0
|
||||
.map(|ctx| {
|
||||
ctx.into_iter()
|
||||
.map(|FieldNavItem { href, text }| {
|
||||
view! { <A href=href>{text}</A> }
|
||||
})
|
||||
.collect_view()
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
|
||||
22
examples/tailwind_axum/package-lock.json
generated
Normal file
22
examples/tailwind_axum/package-lock.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "leptos-tailwind",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "leptos-tailwind",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz",
|
||||
"integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
69
examples/tailwind_axum/src/style.css
Normal file
69
examples/tailwind_axum/src/style.css
Normal file
@@ -0,0 +1,69 @@
|
||||
/*! tailwindcss v4.0.0 | MIT License | https://tailwindcss.com */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.m-auto {
|
||||
margin: auto;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.border-b-4 {
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
.border-l-2 {
|
||||
border-left-style: var(--tw-border-style);
|
||||
border-left-width: 2px;
|
||||
}
|
||||
.bg-gradient-to-tl {
|
||||
--tw-gradient-position: to top left in oklab,;
|
||||
background-image: linear-gradient(var(--tw-gradient-stops));
|
||||
}
|
||||
@property --tw-rotate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateX(0);
|
||||
}
|
||||
@property --tw-rotate-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateY(0);
|
||||
}
|
||||
@property --tw-rotate-z {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateZ(0);
|
||||
}
|
||||
@property --tw-skew-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: skewX(0);
|
||||
}
|
||||
@property --tw-skew-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: skewY(0);
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::todo::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::Path,
|
||||
@@ -8,10 +8,9 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use todo_app_sqlite_axum::*;
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
req: Request<Body>,
|
||||
@@ -20,14 +19,16 @@ async fn custom_handler(
|
||||
move || {
|
||||
provide_context(id.clone());
|
||||
},
|
||||
TodoApp,
|
||||
todo::TodoApp,
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use crate::todo::ssr::db;
|
||||
use crate::todo::{ssr::db, *};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
simple_logger::init_with_level(log::Level::Error)
|
||||
.expect("couldn't initialize logging");
|
||||
@@ -45,7 +46,7 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.route("/special/{id}", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
@@ -61,3 +62,12 @@ async fn main() {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
use leptos::mount::mount_to_body;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(todo::TodoApp);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
leptos_integration_utils = { path = "../../integrations/utils", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.5.1", features = ["util"], optional = true }
|
||||
tower-http = { version = "0.6.1", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn main() {
|
||||
// here, we're not actually doing server side rendering, so we set up a manual
|
||||
// handler for the server fns
|
||||
// this should include a get() handler if you have any GetUrl-based server fns
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/api/{*fn_name}", post(leptos_axum::handle_server_fns))
|
||||
.fallback(file_or_index_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -21,6 +21,7 @@ leptos_macro = { workspace = true, features = ["actix"] }
|
||||
leptos_meta = { workspace = true, features = ["nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
server_fn = { workspace = true, features = ["actix"] }
|
||||
tachys = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
parking_lot = "0.12.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
@@ -33,7 +34,7 @@ once_cell = "1"
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
dont-use-islands-router = []
|
||||
islands-router = ["tachys/islands"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -23,6 +23,7 @@ use hydration_context::SsrSharedContext;
|
||||
use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
hydration::IslandsRouterNavigation,
|
||||
prelude::expect_context,
|
||||
reactive::{computed::ScopedFuture, owner::Owner},
|
||||
IntoView,
|
||||
@@ -367,7 +368,6 @@ pub fn handle_server_fns_with_context(
|
||||
// actually run the server fn
|
||||
let mut res = ActixResponse(
|
||||
service
|
||||
.0
|
||||
.run(ActixRequest::from((req, payload)))
|
||||
.await
|
||||
.take(),
|
||||
@@ -655,12 +655,27 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
_ = replace_blocks; // TODO
|
||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
Box::pin(app.to_html_stream_out_of_order().chain(chunks()))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
})
|
||||
handle_response(
|
||||
method,
|
||||
additional_context,
|
||||
app_fn,
|
||||
|app, chunks, supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
if supports_ooo {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order_branching()
|
||||
}
|
||||
} else if supports_ooo {
|
||||
app.to_html_stream_out_of_order()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -686,12 +701,21 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(method, additional_context, app_fn, |app, chunks| {
|
||||
Box::pin(async move {
|
||||
Box::pin(app.to_html_stream_in_order().chain(chunks()))
|
||||
as PinnedStream<String>
|
||||
})
|
||||
})
|
||||
handle_response(
|
||||
method,
|
||||
additional_context,
|
||||
app_fn,
|
||||
|app, chunks, _supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
@@ -723,12 +747,13 @@ where
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
_supports_ooo: bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -768,6 +793,7 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn handle_response<IV>(
|
||||
method: Method,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -775,6 +801,7 @@ fn handle_response<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> Route
|
||||
where
|
||||
@@ -785,6 +812,9 @@ where
|
||||
let add_context = additional_context.clone();
|
||||
|
||||
async move {
|
||||
let is_island_router_navigation = cfg!(feature = "islands-router")
|
||||
&& req.headers().get("Islands-Router").is_some();
|
||||
|
||||
let res_options = ResponseOptions::default();
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
|
||||
@@ -795,6 +825,10 @@ where
|
||||
move || {
|
||||
provide_contexts(req, &meta_context, &res_options);
|
||||
add_context();
|
||||
|
||||
if is_island_router_navigation {
|
||||
provide_context(IslandsRouterNavigation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -804,6 +838,7 @@ where
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
!is_island_router_navigation,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1094,6 +1129,7 @@ impl StaticRouteGenerator {
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
false,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
@@ -4,14 +4,14 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = { workspace = true }
|
||||
version = "0.8.0-alpha2"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { version = "0.7.9", default-features = false, features = [
|
||||
axum = { version = "0.8.1", default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
@@ -22,6 +22,7 @@ leptos_macro = { workspace = true, features = ["axum"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr", "nonce"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_integration_utils = { workspace = true }
|
||||
tachys = { workspace = true }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12.3"
|
||||
tokio = { version = "1.43", default-features = false }
|
||||
@@ -30,13 +31,19 @@ tower-http = "0.6.2"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.7.9"
|
||||
axum = "0.8.1"
|
||||
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
|
||||
dont-use-islands-router = []
|
||||
default = [
|
||||
"tokio/fs",
|
||||
"tokio/sync",
|
||||
"tower-http/fs",
|
||||
"tower/util",
|
||||
"server_fn/axum",
|
||||
]
|
||||
islands-router = ["tachys/islands"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -370,8 +370,6 @@ async fn handle_server_fns_inner(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
use server_fn::middleware::Service;
|
||||
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
let (req, parts) = generate_request_and_parts(req);
|
||||
@@ -487,7 +485,7 @@ pub type PinnedHtmlStream =
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -511,7 +509,7 @@ where
|
||||
)]
|
||||
pub fn render_route<S, IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
@@ -576,7 +574,7 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -629,13 +627,14 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -658,8 +657,8 @@ where
|
||||
)]
|
||||
pub fn render_route_with_context<S, IV>(
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
State<S>,
|
||||
Request<Body>,
|
||||
@@ -760,25 +759,32 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
_ = replace_blocks; // TODO
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
handle_response(additional_context, app_fn, |app, chunks, supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
if supports_ooo {
|
||||
app.to_html_stream_out_of_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order_branching()
|
||||
}
|
||||
} else if supports_ooo {
|
||||
app.to_html_stream_out_of_order()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
};
|
||||
Box::pin(app.chain(chunks())) as PinnedStream<String>
|
||||
})
|
||||
@@ -827,8 +833,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -838,8 +844,8 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -851,13 +857,18 @@ where
|
||||
}
|
||||
|
||||
fn handle_response<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>> + Clone + Send + 'static
|
||||
) -> impl Fn(Request<Body>) -> PinnedFuture<Response<Body>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -875,12 +886,16 @@ fn handle_response_inner<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
) -> PinnedFuture<Response<Body>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let is_island_router_navigation = cfg!(feature = "islands-router")
|
||||
&& req.headers().get("Islands-Router").is_some();
|
||||
|
||||
let add_context = additional_context.clone();
|
||||
let res_options = ResponseOptions::default();
|
||||
let (meta_context, meta_output) = ServerMetaContext::new();
|
||||
@@ -902,6 +917,10 @@ where
|
||||
res_options.clone(),
|
||||
);
|
||||
add_context();
|
||||
|
||||
if is_island_router_navigation {
|
||||
provide_context(IslandsRouterNavigation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -911,6 +930,7 @@ where
|
||||
additional_context,
|
||||
res_options,
|
||||
stream_builder,
|
||||
!is_island_router_navigation,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -985,7 +1005,7 @@ fn provide_contexts(
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async<IV>(
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1039,8 +1059,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_stream_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1050,9 +1070,9 @@ pub fn render_app_async_stream_with_context<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
handle_response(additional_context, app_fn, |app, chunks| {
|
||||
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -1106,8 +1126,8 @@ where
|
||||
tracing::instrument(level = "trace", fields(error), skip_all)
|
||||
)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
|
||||
@@ -1123,12 +1143,13 @@ where
|
||||
fn async_stream_builder<IV>(
|
||||
app: IV,
|
||||
chunks: BoxedFnOnce<PinnedStream<String>>,
|
||||
_supports_ooo: bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let app = if cfg!(feature = "dont-use-islands-router") {
|
||||
let app = if cfg!(feature = "islands-router") {
|
||||
app.to_html_stream_in_order_branching()
|
||||
} else {
|
||||
app.to_html_stream_in_order()
|
||||
@@ -1401,6 +1422,7 @@ impl StaticRouteGenerator {
|
||||
app_fn.clone(),
|
||||
additional_context,
|
||||
async_stream_builder,
|
||||
false,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
@@ -1646,7 +1668,7 @@ where
|
||||
self,
|
||||
options: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
@@ -1661,8 +1683,8 @@ where
|
||||
self,
|
||||
options: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
@@ -1695,12 +1717,15 @@ impl AxumPath for Vec<PathSegment> {
|
||||
match segment {
|
||||
PathSegment::Static(s) => path.push_str(s),
|
||||
PathSegment::Param(s) => {
|
||||
path.push(':');
|
||||
path.push('{');
|
||||
path.push_str(s);
|
||||
path.push('}');
|
||||
}
|
||||
PathSegment::Splat(s) => {
|
||||
path.push('{');
|
||||
path.push('*');
|
||||
path.push_str(s);
|
||||
path.push('}');
|
||||
}
|
||||
PathSegment::Unit => {}
|
||||
PathSegment::OptionalParam(_) => {
|
||||
@@ -1732,7 +1757,7 @@ where
|
||||
self,
|
||||
state: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -1748,8 +1773,8 @@ where
|
||||
self,
|
||||
state: &S,
|
||||
paths: Vec<AxumRouteListing>,
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||
additional_context: impl Fn() + 'static + Clone + Send + Sync,
|
||||
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -1830,64 +1855,64 @@ where
|
||||
}
|
||||
} else {
|
||||
router.route(
|
||||
path,
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
path,
|
||||
match listing.mode() {
|
||||
SsrMode::OutOfOrder => {
|
||||
let s = render_app_to_stream_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
SsrMode::PartiallyBlocked => {
|
||||
let s = render_app_to_stream_with_context_and_replace_blocks(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
true
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
SsrMode::InOrder => {
|
||||
let s = render_app_to_stream_in_order_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
SsrMode::Async => {
|
||||
let s = render_app_async_with_context(
|
||||
cx_with_state_and_method.clone(),
|
||||
app_fn.clone(),
|
||||
);
|
||||
match method {
|
||||
leptos_router::Method::Get => get(s),
|
||||
leptos_router::Method::Post => post(s),
|
||||
leptos_router::Method::Put => put(s),
|
||||
leptos_router::Method::Delete => delete(s),
|
||||
leptos_router::Method::Patch => patch(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
_ => unreachable!()
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2021,7 +2046,7 @@ where
|
||||
},
|
||||
move || shell(options),
|
||||
req,
|
||||
|app, chunks| {
|
||||
|app, chunks, _supports_ooo| {
|
||||
Box::pin(async move {
|
||||
let app = app
|
||||
.to_html_stream_in_order()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use futures::{stream::once, Stream, StreamExt};
|
||||
use hydration_context::{SharedContext, SsrSharedContext};
|
||||
use leptos::{
|
||||
@@ -31,14 +33,20 @@ pub trait ExtendResponse: Sized {
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
supports_ooo: bool,
|
||||
) -> impl Future<Output = Self> + Send
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
async move {
|
||||
let (owner, stream) =
|
||||
build_response(app_fn, additional_context, stream_builder);
|
||||
let (owner, stream) = build_response(
|
||||
app_fn,
|
||||
additional_context,
|
||||
stream_builder,
|
||||
supports_ooo,
|
||||
);
|
||||
|
||||
let sc = owner.shared_context().unwrap();
|
||||
|
||||
@@ -94,7 +102,11 @@ pub fn build_response<IV>(
|
||||
stream_builder: fn(
|
||||
IV,
|
||||
BoxedFnOnce<PinnedStream<String>>,
|
||||
// this argument indicates whether a request wants to support out-of-order streaming
|
||||
// responses
|
||||
bool,
|
||||
) -> PinnedFuture<PinnedStream<String>>,
|
||||
is_islands_router_navigation: bool,
|
||||
) -> (Owner, PinnedFuture<PinnedStream<String>>)
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
@@ -138,7 +150,7 @@ where
|
||||
//
|
||||
// we also don't actually start hydrating until after the whole stream is complete,
|
||||
// so it's not useful to send those scripts down earlier.
|
||||
stream_builder(app, chunks)
|
||||
stream_builder(app, chunks, is_islands_router_navigation)
|
||||
});
|
||||
|
||||
stream.await
|
||||
|
||||
@@ -42,11 +42,7 @@ typed-builder = "0.20.0"
|
||||
typed-builder-macro = "0.20.0"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
server_fn = { workspace = true, features = [
|
||||
"form-redirects",
|
||||
"browser",
|
||||
"url",
|
||||
] }
|
||||
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
@@ -100,6 +96,15 @@ trace-component-props = [
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
|
||||
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
|
||||
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
|
||||
# This is automatic as long as the leptos crate is depended upon,
|
||||
# downstream usage should never manually enable this feature.
|
||||
[target.'cfg(erase_components)'.dependencies]
|
||||
leptos_macro = { workspace = true, features = ["__internal_erase_components"] }
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
"nightly",
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn AttributeInterceptor<Chil, T>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
Chil: Fn(AnyAttribute) -> T + Send + Sync + 'static,
|
||||
T: IntoView,
|
||||
T: IntoView + 'static,
|
||||
{
|
||||
AttributeInterceptorInner::new(children)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ impl<T: IntoView, A: Attribute> Render for AttributeInterceptorInner<T, A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView, A> AddAnyAttr for AttributeInterceptorInner<T, A>
|
||||
impl<T: IntoView + 'static, A> AddAnyAttr for AttributeInterceptorInner<T, A>
|
||||
where
|
||||
A: Attribute,
|
||||
{
|
||||
@@ -114,8 +114,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
|
||||
impl<T: IntoView + 'static, A: Attribute> RenderHtml
|
||||
for AttributeInterceptorInner<T, A>
|
||||
{
|
||||
type AsyncOutput = T::AsyncOutput;
|
||||
type Owned = AttributeInterceptorInner<T, A::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = T::MIN_LENGTH;
|
||||
|
||||
@@ -135,9 +138,15 @@ impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
|
||||
position: &mut leptos::tachys::view::Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
self.children
|
||||
.to_html_with_buf(buf, position, escape, mark_branches)
|
||||
self.children.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
vec![],
|
||||
)
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
@@ -147,4 +156,12 @@ impl<T: IntoView, A: Attribute> RenderHtml for AttributeInterceptorInner<T, A> {
|
||||
) -> Self::State {
|
||||
self.children.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
AttributeInterceptorInner {
|
||||
children_builder: self.children_builder,
|
||||
children: self.children,
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,20 @@
|
||||
|
||||
use reactive_graph::{
|
||||
owner::{LocalStorage, StoredValue},
|
||||
traits::WithValue,
|
||||
traits::{Dispose, WithValue},
|
||||
};
|
||||
use std::{fmt, rc::Rc, sync::Arc};
|
||||
|
||||
/// A wrapper trait for calling callbacks.
|
||||
pub trait Callable<In: 'static, Out: 'static = ()> {
|
||||
/// calls the callback with the specified argument.
|
||||
///
|
||||
/// Returns None if the callback has been disposed
|
||||
fn try_run(&self, input: In) -> Option<Out>;
|
||||
/// calls the callback with the specified argument.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to run a callback that has been disposed
|
||||
fn run(&self, input: In) -> Out;
|
||||
}
|
||||
|
||||
@@ -72,6 +79,12 @@ impl<In, Out> Clone for UnsyncCallback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Dispose for UnsyncCallback<In, Out> {
|
||||
fn dispose(self) {
|
||||
self.0.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> UnsyncCallback<In, Out> {
|
||||
/// Creates a new callback from the given function.
|
||||
pub fn new<F>(f: F) -> UnsyncCallback<In, Out>
|
||||
@@ -93,6 +106,10 @@ impl<In, Out> UnsyncCallback<In, Out> {
|
||||
}
|
||||
|
||||
impl<In: 'static, Out: 'static> Callable<In, Out> for UnsyncCallback<In, Out> {
|
||||
fn try_run(&self, input: In) -> Option<Out> {
|
||||
self.0.try_with_value(|fun| fun(input))
|
||||
}
|
||||
|
||||
fn run(&self, input: In) -> Out {
|
||||
self.0.with_value(|fun| fun(input))
|
||||
}
|
||||
@@ -168,10 +185,12 @@ impl<In, Out> fmt::Debug for Callback<In, Out> {
|
||||
}
|
||||
|
||||
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
|
||||
fn try_run(&self, input: In) -> Option<Out> {
|
||||
self.0.try_with_value(|fun| fun(input))
|
||||
}
|
||||
|
||||
fn run(&self, input: In) -> Out {
|
||||
self.0
|
||||
.try_with_value(|f| f(input))
|
||||
.expect("called a callback that has been disposed")
|
||||
self.0.with_value(|f| f(input))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +200,12 @@ impl<In, Out> Clone for Callback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Dispose for Callback<In, Out> {
|
||||
fn dispose(self) {
|
||||
self.0.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Out> Copy for Callback<In, Out> {}
|
||||
|
||||
macro_rules! impl_callable_from_fn {
|
||||
@@ -239,7 +264,9 @@ impl<In: 'static, Out: 'static> Callback<In, Out> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Callable;
|
||||
use crate::callback::{Callback, UnsyncCallback};
|
||||
use reactive_graph::traits::Dispose;
|
||||
|
||||
struct NoClone {}
|
||||
|
||||
@@ -270,6 +297,22 @@ mod tests {
|
||||
(|num, s| format!("{num} {s}")).into();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_try_run() {
|
||||
let callback = Callback::new(move |arg| arg);
|
||||
assert_eq!(callback.try_run((0,)), Some((0,)));
|
||||
callback.dispose();
|
||||
assert_eq!(callback.try_run((0,)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsync_callback_try_run() {
|
||||
let callback = UnsyncCallback::new(move |arg| arg);
|
||||
assert_eq!(callback.try_run((0,)), Some((0,)));
|
||||
callback.dispose();
|
||||
assert_eq!(callback.try_run((0,)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_matches_same() {
|
||||
let callback1 = Callback::new(|x: i32| x * 2);
|
||||
|
||||
@@ -11,7 +11,7 @@ use reactive_graph::{
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
reactive_graph::OwnedView,
|
||||
ssr::StreamBuilder,
|
||||
@@ -163,6 +163,14 @@ where
|
||||
self.children.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
|
||||
if let Some(fallback) = &self.fallback {
|
||||
fallback.elements()
|
||||
} else {
|
||||
self.children.elements()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Chil, FalFn, Fal> Render for ErrorBoundaryView<Chil, FalFn>
|
||||
@@ -268,6 +276,7 @@ where
|
||||
Fal: RenderHtml + Send + 'static,
|
||||
{
|
||||
type AsyncOutput = ErrorBoundaryView<Chil::AsyncOutput, FalFn>;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
|
||||
|
||||
@@ -301,6 +310,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let _hook = throw_error::set_error_hook(self.hook);
|
||||
@@ -311,6 +321,7 @@ where
|
||||
&mut new_pos,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
@@ -323,6 +334,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -333,6 +345,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -345,6 +358,7 @@ where
|
||||
&mut new_pos,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
@@ -358,6 +372,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
buf.push_sync(&fallback);
|
||||
}
|
||||
@@ -423,6 +438,10 @@ where
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -6,7 +6,10 @@ use reactive_graph::{
|
||||
traits::Set,
|
||||
};
|
||||
use std::hash::Hash;
|
||||
use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
|
||||
use tachys::{
|
||||
reactive_graph::OwnedView,
|
||||
view::keyed::{keyed, SerializableKey},
|
||||
};
|
||||
|
||||
/// Iterates over children and displays them, keyed by the `key` function given.
|
||||
///
|
||||
@@ -121,7 +124,7 @@ where
|
||||
EF: Fn(T) -> N + Send + Clone + 'static,
|
||||
N: IntoView + 'static,
|
||||
KF: Fn(&T) -> K + Send + Clone + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
K: Eq + Hash + SerializableKey + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
// this takes the owner of the For itself
|
||||
@@ -195,7 +198,7 @@ where
|
||||
EF: Fn(ReadSignal<usize>, T) -> N + Send + Clone + 'static,
|
||||
N: IntoView + 'static,
|
||||
KF: Fn(&T) -> K + Send + Clone + 'static,
|
||||
K: Eq + Hash + 'static,
|
||||
K: Eq + Hash + SerializableKey + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
// this takes the owner of the For itself
|
||||
@@ -218,6 +221,7 @@ where
|
||||
};
|
||||
move || keyed(each(), key.clone(), children.clone())
|
||||
}
|
||||
|
||||
/*
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -3,7 +3,11 @@ use leptos_dom::helpers::window;
|
||||
use leptos_server::{ServerAction, ServerMultiAction};
|
||||
use serde::de::DeserializeOwned;
|
||||
use server_fn::{
|
||||
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
|
||||
client::Client,
|
||||
codec::PostUrl,
|
||||
error::{IntoAppError, ServerFnErrorErr},
|
||||
request::ClientReq,
|
||||
Http, ServerFn,
|
||||
};
|
||||
use tachys::{
|
||||
either::Either,
|
||||
@@ -71,7 +75,7 @@ use web_sys::{
|
||||
/// ```
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
pub fn ActionForm<ServFn>(
|
||||
pub fn ActionForm<ServFn, OutputProtocol>(
|
||||
/// The action from which to build the form.
|
||||
action: ServerAction<ServFn>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
@@ -82,7 +86,7 @@ pub fn ActionForm<ServFn>(
|
||||
) -> impl IntoView
|
||||
where
|
||||
ServFn: DeserializeOwned
|
||||
+ ServerFn<InputEncoding = PostUrl>
|
||||
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
@@ -121,9 +125,10 @@ where
|
||||
"Error converting form field into server function \
|
||||
arguments: {err:?}"
|
||||
);
|
||||
value.set(Some(Err(ServerFnError::Serialization(
|
||||
value.set(Some(Err(ServerFnErrorErr::Serialization(
|
||||
err.to_string(),
|
||||
))));
|
||||
)
|
||||
.into_app_error())));
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
}
|
||||
@@ -146,7 +151,7 @@ where
|
||||
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
|
||||
/// progressively enhanced to use client-side routing.
|
||||
#[component]
|
||||
pub fn MultiActionForm<ServFn>(
|
||||
pub fn MultiActionForm<ServFn, OutputProtocol>(
|
||||
/// The action from which to build the form.
|
||||
action: ServerMultiAction<ServFn>,
|
||||
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||
@@ -160,7 +165,7 @@ where
|
||||
+ Sync
|
||||
+ Clone
|
||||
+ DeserializeOwned
|
||||
+ ServerFn<InputEncoding = PostUrl>
|
||||
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
|
||||
+ 'static,
|
||||
ServFn::Output: Send + Sync + 'static,
|
||||
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
|
||||
@@ -187,9 +192,10 @@ where
|
||||
action.dispatch(new_input);
|
||||
}
|
||||
Err(err) => {
|
||||
action.dispatch_sync(Err(ServerFnError::Serialization(
|
||||
action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
|
||||
err.to_string(),
|
||||
)));
|
||||
)
|
||||
.into_app_error()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
mod.hydrate();
|
||||
hydrateIslands(document.body, mod);
|
||||
});
|
||||
|
||||
window.__hydrateIsland = (el, id) => hydrateIsland(el, id, mod);
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
378
leptos/src/hydration/islands_routing.js
Normal file
378
leptos/src/hydration/islands_routing.js
Normal file
@@ -0,0 +1,378 @@
|
||||
let NAVIGATION = 0;
|
||||
|
||||
window.addEventListener("click", async (ev) => {
|
||||
const req = clickToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true);
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", async (ev) => {
|
||||
const req = new Request(window.location);
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true, true);
|
||||
});
|
||||
|
||||
window.addEventListener("submit", async (ev) => {
|
||||
const req = submitToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
await navigateToPage(req, true);
|
||||
});
|
||||
|
||||
async function navigateToPage(
|
||||
/** @type Request */
|
||||
req,
|
||||
/** @type bool */
|
||||
useViewTransition,
|
||||
/** @type bool */
|
||||
replace
|
||||
) {
|
||||
NAVIGATION += 1;
|
||||
const currentNav = NAVIGATION;
|
||||
|
||||
// add a custom header to indicate that we're on a subsequent navigation
|
||||
req.headers.append("Islands-Router", "true");
|
||||
|
||||
// fetch the new page
|
||||
const resp = await fetch(req);
|
||||
const redirected = resp.redirected;
|
||||
const htmlString = await resp.text();
|
||||
|
||||
if(NAVIGATION === currentNav) {
|
||||
// The 'doc' variable now contains the parsed DOM
|
||||
const transition = async () => {
|
||||
try {
|
||||
diffPages(htmlString);
|
||||
for(const island of document.querySelectorAll("leptos-island")) {
|
||||
if(!island.$$hydrated) {
|
||||
__hydrateIsland(island, island.dataset.component);
|
||||
island.$$hydrated = true;
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
// Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition
|
||||
if (useViewTransition && document.startViewTransition) {
|
||||
await document.startViewTransition(transition);
|
||||
} else {
|
||||
await transition()
|
||||
}
|
||||
|
||||
const url = redirected ? resp.url : req.url;
|
||||
|
||||
if(replace) {
|
||||
window.history.replaceState(undefined, null, url);
|
||||
} else {
|
||||
window.history.pushState(undefined, null, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clickToReq(ev) {
|
||||
// confirm that this is an <a> that meets our requirements
|
||||
if (
|
||||
ev.defaultPrevented ||
|
||||
ev.button !== 0 ||
|
||||
ev.metaKey ||
|
||||
ev.altKey ||
|
||||
ev.ctrlKey ||
|
||||
ev.shiftKey
|
||||
)
|
||||
return;
|
||||
|
||||
/** @type HTMLAnchorElement | undefined;*/
|
||||
const a = ev
|
||||
.composedPath()
|
||||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
||||
|
||||
if (!a) return;
|
||||
|
||||
const svg = a.namespaceURI === "http://www.w3.org/2000/svg";
|
||||
const href = svg ? a.href.baseVal : a.href;
|
||||
const target = svg ? a.target.baseVal : a.target;
|
||||
if (target || (!href && !a.hasAttribute("state"))) return;
|
||||
|
||||
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
||||
if (a.hasAttribute("download") || (rel?.includes("external"))) return;
|
||||
|
||||
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
||||
if (
|
||||
url.origin !== window.location.origin // ||
|
||||
// TODO base
|
||||
//(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))
|
||||
)
|
||||
return;
|
||||
|
||||
return new Request(url);
|
||||
}
|
||||
|
||||
function submitToReq(ev) {
|
||||
event.preventDefault();
|
||||
|
||||
const target = ev.target;
|
||||
/** @type HTMLFormElement */
|
||||
let form;
|
||||
if(target instanceof HTMLFormElement) {
|
||||
form = target;
|
||||
} else {
|
||||
if(!target.form) {
|
||||
return;
|
||||
}
|
||||
form = target.form;
|
||||
}
|
||||
|
||||
const method = form.method.toUpperCase();
|
||||
if(method !== "GET" && method !== "POST") {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(form.action);
|
||||
let path = url.pathname;
|
||||
const requestInit = {};
|
||||
const data = new FormData(form);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of data.entries()) {
|
||||
params.append(key, value);
|
||||
}
|
||||
|
||||
requestInit.headers = {
|
||||
Accept: "text/html"
|
||||
};
|
||||
if(method === "GET") {
|
||||
path += `?${params.toString()}`;
|
||||
}
|
||||
else {
|
||||
requestInit.method = "POST";
|
||||
requestInit.body = params;
|
||||
}
|
||||
|
||||
return new Request(
|
||||
path,
|
||||
requestInit
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function diffPages(htmlString) {
|
||||
// Use DOMParser to parse the HTML string
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
diffRange(document, document, doc, doc);
|
||||
}
|
||||
|
||||
function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
|
||||
const oldDocWalker = oldDocument.createTreeWalker(oldRoot);
|
||||
const newDocWalker = newDocument.createTreeWalker(newRoot);
|
||||
let oldNode = oldDocWalker.currentNode;
|
||||
let newNode = newDocWalker.currentNode;
|
||||
|
||||
while (oldDocWalker.nextNode() && newDocWalker.nextNode()) {
|
||||
oldNode = oldDocWalker.currentNode;
|
||||
newNode = newDocWalker.currentNode;
|
||||
|
||||
if (oldNode == oldEnd || newNode == newEnd) {
|
||||
break;
|
||||
}
|
||||
|
||||
// if the nodes are different, we need to replace the old with the new
|
||||
// because of the typed view tree, this should never actually happen
|
||||
if (oldNode.nodeType !== newNode.nodeType) {
|
||||
oldNode.replaceWith(newNode);
|
||||
}
|
||||
// if it's a text node, just update the text with the new text
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// if it's an element, replace if it's a different tag, or update attributes
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
|
||||
diffElement(oldNode, newNode);
|
||||
}
|
||||
// we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree
|
||||
// if one of these marker is hit, then there are two options
|
||||
// 1) it's the same branch, and we just keep walking until the end
|
||||
// 2) it's a different branch, in which case the old can be replaced with the new wholesale
|
||||
else if (oldNode.nodeType === Node.COMMENT_NODE) {
|
||||
const oldText = oldNode.textContent;
|
||||
const newText = newNode.textContent;
|
||||
if(oldText.startsWith("bo-for")) {
|
||||
replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode);
|
||||
}
|
||||
else if (oldText.startsWith("bo-item")) {
|
||||
// skip, this means we're diffing a new item within a For
|
||||
}
|
||||
else if(oldText.startsWith("bo") && newText !== oldText) {
|
||||
replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
|
||||
const oldKeys = {};
|
||||
const newKeys = {};
|
||||
|
||||
while(oldBranches > 0) {
|
||||
const c = oldDocWalker.currentNode;
|
||||
if(c.nodeType === Node.COMMENT_NODE) {
|
||||
const t = c.textContent;
|
||||
if(t.startsWith("bo-for")) {
|
||||
oldBranches += 1;
|
||||
} else if(t.startsWith("bc-for")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
} else if (t.startsWith("bo-item")) {
|
||||
const k = t.replace("bo-item-", "");
|
||||
oldKeys[k] = { open: c, close: null };
|
||||
} else if (t.startsWith("bc-item")) {
|
||||
const k = t.replace("bc-item-", "");
|
||||
oldKeys[k].close = c;
|
||||
}
|
||||
}
|
||||
oldDocWalker.nextNode();
|
||||
}
|
||||
while(newBranches > 0) {
|
||||
const c = newDocWalker.currentNode;
|
||||
if(c.nodeType === Node.COMMENT_NODE) {
|
||||
const t = c.textContent;
|
||||
if(t.startsWith("bo-for")) {
|
||||
newBranches += 1;
|
||||
} else if(t.startsWith("bc-for")) {
|
||||
|
||||
newBranches -= 1;
|
||||
} else if (t.startsWith("bo-item")) {
|
||||
const k = t.replace("bo-item-", "");
|
||||
newKeys[k] = { open: c, close: null };
|
||||
} else if (t.startsWith("bc-item")) {
|
||||
const k = t.replace("bc-item-", "");
|
||||
newKeys[k].close = c;
|
||||
}
|
||||
}
|
||||
newDocWalker.nextNode();
|
||||
}
|
||||
|
||||
for(const key in oldKeys) {
|
||||
if(newKeys[key]) {
|
||||
const oldOne = oldKeys[key];
|
||||
const newOne = newKeys[key];
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
|
||||
// then replace the item in the *new* list with the *old* DOM elements
|
||||
oldRange.setStartAfter(oldOne.open);
|
||||
oldRange.setEndBefore(oldOne.close);
|
||||
newRange.setStartAfter(newOne.open);
|
||||
newRange.setEndBefore(newOne.close);
|
||||
const oldContents = oldRange.extractContents();
|
||||
const newContents = newRange.extractContents();
|
||||
|
||||
// patch the *old* DOM elements with the new ones
|
||||
diffRange(oldDocument, oldContents, newDocument, newContents, oldOne.close, newOne.close);
|
||||
|
||||
// then insert the old DOM elements into the new tree
|
||||
// this means you'll end up with any new attributes or content from the server,
|
||||
// but with any old DOM state (because they are the old elements)
|
||||
newRange.insertNode(oldContents);
|
||||
newOne.open.replaceWith(oldOne.open);
|
||||
newOne.close.replaceWith(oldOne.close);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndAfter(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode) {
|
||||
oldDocWalker.nextNode();
|
||||
newDocWalker.nextNode();
|
||||
const oldRange = new Range();
|
||||
const newRange = new Range();
|
||||
let oldBranches = 1;
|
||||
let newBranches = 1;
|
||||
while(oldBranches > 0) {
|
||||
if(oldDocWalker.nextNode()) {
|
||||
if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(oldDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
oldBranches += 1;
|
||||
} else if(oldDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
oldBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while(newBranches > 0) {
|
||||
if(newDocWalker.nextNode()) {
|
||||
if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) {
|
||||
if(newDocWalker.currentNode.textContent.startsWith("bo")) {
|
||||
newBranches += 1;
|
||||
} else if(newDocWalker.currentNode.textContent.startsWith("bc")) {
|
||||
|
||||
newBranches -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oldRange.setStartAfter(oldNode);
|
||||
oldRange.setEndBefore(oldDocWalker.currentNode);
|
||||
newRange.setStartAfter(newNode);
|
||||
newRange.setEndAfter(newDocWalker.currentNode);
|
||||
const newContents = newRange.extractContents();
|
||||
oldRange.deleteContents();
|
||||
oldRange.insertNode(newContents);
|
||||
oldNode.replaceWith(newNode);
|
||||
oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function diffElement(oldNode, newNode) {
|
||||
/** @type Element */
|
||||
const oldEl = oldNode;
|
||||
/** @type Element */
|
||||
const newEl = newNode;
|
||||
if (oldEl.tagName !== newEl.tagName) {
|
||||
oldEl.replaceWith(newEl);
|
||||
|
||||
}
|
||||
else {
|
||||
for(const attr of newEl.attributes) {
|
||||
oldEl.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(const island of document.querySelectorAll("leptos-island")) {
|
||||
island.$$hydrated = true;
|
||||
}
|
||||
@@ -50,6 +50,10 @@ pub fn HydrationScripts(
|
||||
/// Should be `true` to hydrate in `islands` mode.
|
||||
#[prop(optional)]
|
||||
islands: bool,
|
||||
/// Should be `true` to add the “islands router,” which enables limited client-side routing
|
||||
/// when running in islands mode.
|
||||
#[prop(optional)]
|
||||
islands_router: bool,
|
||||
/// A base url, not including a trailing slash
|
||||
#[prop(optional, into)]
|
||||
root: Option<String>,
|
||||
@@ -98,18 +102,36 @@ pub fn HydrationScripts(
|
||||
include_str!("./hydration_script.js")
|
||||
};
|
||||
|
||||
let islands_router = islands_router
|
||||
.then_some(include_str!("./islands_routing.js"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let root = root.unwrap_or_default();
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?})")}
|
||||
</script>
|
||||
}
|
||||
use_context::<IslandsRouterNavigation>().is_none().then(|| {
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
|
||||
</script>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// If this is provided via context, it means that you are using the islands router and
|
||||
/// this is a subsequent navigation, made from the client.
|
||||
///
|
||||
/// This should be provided automatically by a server integration if it detects that the
|
||||
/// header `Islands-Router` is present in the request.
|
||||
///
|
||||
/// This is used to determine how much of the hydration script to include in the page.
|
||||
/// If it is present, then the contents of the `<HydrationScripts>` component will not be
|
||||
/// included, as they only need to be sent to the client once.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IslandsRouterNavigation;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
ssr::StreamBuilder,
|
||||
view::{
|
||||
@@ -87,6 +87,7 @@ impl<T: Render> Render for View<T> {
|
||||
|
||||
impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
type AsyncOutput = T::AsyncOutput;
|
||||
type Owned = View<T::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = <T as RenderHtml>::MIN_LENGTH;
|
||||
|
||||
@@ -104,6 +105,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
let vm = self.view_marker.to_owned();
|
||||
@@ -112,8 +114,13 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
buf.push_str(&format!("<!--hot-reload|{vm}|open-->"));
|
||||
}
|
||||
|
||||
self.inner
|
||||
.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
self.inner.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(vm) = vm.as_ref() {
|
||||
@@ -127,6 +134,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -142,6 +150,7 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -157,6 +166,14 @@ impl<T: RenderHtml> RenderHtml for View<T> {
|
||||
) -> Self::State {
|
||||
self.inner.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
View {
|
||||
inner: self.inner.into_owned(),
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: self.view_marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToTemplate> ToTemplate for View<T> {
|
||||
|
||||
@@ -172,12 +172,10 @@ pub mod prelude {
|
||||
actions::*, computed::*, effect::*, graph::untrack, owner::*,
|
||||
signal::*, wrappers::read::*,
|
||||
};
|
||||
pub use server_fn::{self, ServerFnError};
|
||||
pub use server_fn::{self, error::ServerFnError};
|
||||
pub use tachys::{
|
||||
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
|
||||
view::{
|
||||
any_view::AnyView, fragment::Fragment, template::ViewTemplate,
|
||||
},
|
||||
view::{fragment::Fragment, template::ViewTemplate},
|
||||
};
|
||||
}
|
||||
pub use export_types::*;
|
||||
|
||||
@@ -19,7 +19,7 @@ use slotmap::{DefaultKey, SlotMap};
|
||||
use std::sync::Arc;
|
||||
use tachys::{
|
||||
either::Either,
|
||||
html::attribute::Attribute,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
reactive_graph::{OwnedView, OwnedViewState},
|
||||
ssr::StreamBuilder,
|
||||
@@ -247,6 +247,7 @@ where
|
||||
// i.e., if this is the child of another Suspense during SSR, don't wait for it: it will handle
|
||||
// itself
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
|
||||
|
||||
@@ -262,9 +263,15 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
self.fallback
|
||||
.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
self.fallback.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
@@ -273,6 +280,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -371,6 +379,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
Some(None) => {
|
||||
@@ -380,6 +389,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
@@ -393,12 +403,14 @@ where
|
||||
self.fallback,
|
||||
&mut fallback_position,
|
||||
mark_branches,
|
||||
extra_attrs.clone(),
|
||||
);
|
||||
buf.push_async_out_of_order_with_nonce(
|
||||
fut,
|
||||
position,
|
||||
mark_branches,
|
||||
nonce_or_not(),
|
||||
extra_attrs,
|
||||
);
|
||||
} else {
|
||||
buf.push_async({
|
||||
@@ -414,6 +426,7 @@ where
|
||||
&mut position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
builder.finish().take_chunks()
|
||||
}
|
||||
@@ -463,6 +476,10 @@ where
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper that prevents [`Suspense`] from waiting for any resource reads that happen inside
|
||||
@@ -515,6 +532,7 @@ where
|
||||
T: RenderHtml + 'static,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = T::MIN_LENGTH;
|
||||
|
||||
@@ -530,8 +548,15 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
(self.0)().to_html_with_buf(buf, position, escape, mark_branches);
|
||||
(self.0)().to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
@@ -540,6 +565,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -548,6 +574,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -558,4 +585,8 @@ where
|
||||
) -> Self::State {
|
||||
(self.0)().hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@ impl Default for TextProp {
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for TextProp {
|
||||
type Output = Oco<'static, str>;
|
||||
type Output = Arc<dyn Fn() -> Oco<'static, str> + Send + Sync>;
|
||||
|
||||
fn into_attribute_value(self) -> Self::Output {
|
||||
self.get()
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
/// occur with LeptosOptions
|
||||
#[derive(Clone, Debug, serde::Deserialize, Default)]
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
pub struct ConfFile {
|
||||
pub leptos_options: LeptosOptions,
|
||||
}
|
||||
@@ -24,9 +25,14 @@ pub struct ConfFile {
|
||||
/// It shares keys with cargo-leptos, to allow for easy interoperability
|
||||
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into), default=default_output_name())]
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen.
|
||||
///
|
||||
/// This should match the name that will be output when building your application.
|
||||
///
|
||||
/// You can easily set this using `env!("CARGO_CRATE_NAME")`.
|
||||
#[builder(setter(into))]
|
||||
pub output_name: Arc<str>,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
@@ -78,6 +84,40 @@ pub struct LeptosOptions {
|
||||
#[builder(default = default_hash_files())]
|
||||
#[serde(default = "default_hash_files")]
|
||||
pub hash_files: bool,
|
||||
/// The default prefix to use for server functions when generating API routes. Can be
|
||||
/// overridden for individual functions using `#[server(prefix = "...")]` as usual.
|
||||
///
|
||||
/// This is useful to override the default prefix (`/api`) for all server functions without
|
||||
/// needing to manually specify via `#[server(prefix = "...")]` on every server function.
|
||||
#[builder(default, setter(strip_option))]
|
||||
#[serde(default)]
|
||||
pub server_fn_prefix: Option<String>,
|
||||
/// Whether to disable appending the server functions' hashes to the end of their API names.
|
||||
///
|
||||
/// This is useful when an app's client side needs a stable server API. For example, shipping
|
||||
/// the CSR WASM binary in a Tauri app. Tauri app releases are dependent on each platform's
|
||||
/// distribution method (e.g., the Apple App Store or the Google Play Store), which typically
|
||||
/// are much slower than the frequency at which a website can be updated. In addition, it's
|
||||
/// common for users to not have the latest app version installed. In these cases, the CSR WASM
|
||||
/// app would need to be able to continue calling the backend server function API, so the API
|
||||
/// path needs to be consistent and not have a hash appended.
|
||||
///
|
||||
/// Note that the hash suffixes is intended as a way to ensure duplicate API routes are created.
|
||||
/// Without the hash, server functions will need to have unique names to avoid creating
|
||||
/// duplicate routes. Axum will throw an error if a duplicate route is added to the router, but
|
||||
/// Actix will not.
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub disable_server_fn_hash: bool,
|
||||
/// Include the module path of the server function in the API route. This is an alternative
|
||||
/// strategy to prevent duplicate server function API routes (the default strategy is to add
|
||||
/// a hash to the end of the route). Each element of the module path will be separated by a `/`.
|
||||
/// For example, a server function with a fully qualified name of `parent::child::server_fn`
|
||||
/// would have an API route of `/api/parent/child/server_fn` (possibly with a
|
||||
/// different prefix and a hash suffix depending on the values of the other server fn configs).
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub server_fn_mod_path: bool,
|
||||
}
|
||||
|
||||
impl LeptosOptions {
|
||||
@@ -120,20 +160,14 @@ impl LeptosOptions {
|
||||
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
|
||||
.into(),
|
||||
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
|
||||
server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
|
||||
disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
|
||||
.is_some(),
|
||||
server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LeptosOptions {
|
||||
fn default() -> Self {
|
||||
LeptosOptions::builder().build()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_name() -> Arc<str> {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_").into()
|
||||
}
|
||||
|
||||
fn default_site_root() -> Arc<str> {
|
||||
".".into()
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ trace-component-props = []
|
||||
actix = ["server_fn_macro/actix"]
|
||||
axum = ["server_fn_macro/axum"]
|
||||
generic = ["server_fn_macro/generic"]
|
||||
# Having an erasure feature rather than normal --cfg erase_components for the proc macro crate is a workaround for this rust issue:
|
||||
# https://github.com/rust-lang/cargo/issues/4423
|
||||
# TLDR proc macros will ignore RUSTFLAGS when --target is specified on the cargo command.
|
||||
# This works around the issue by the non proc-macro crate which does see RUSTFLAGS enabling the replacement feature on the proc-macro crate, which wouldn't.
|
||||
# This is automatic as long as the leptos crate is depended upon,
|
||||
# downstream usage should never manually enable this feature.
|
||||
__internal_erase_components = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing", "trace-component-props"]
|
||||
@@ -83,9 +90,3 @@ skip_feature_sets = [
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(erase_components)',
|
||||
] }
|
||||
|
||||
@@ -32,6 +32,8 @@ pub struct Model {
|
||||
impl Parse for Model {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let mut item = ItemFn::parse(input)?;
|
||||
maybe_modify_return_type(&mut item.sig.output);
|
||||
|
||||
convert_impl_trait_to_generic(&mut item.sig);
|
||||
|
||||
let docs = Docs::new(&item.attrs);
|
||||
@@ -76,6 +78,39 @@ impl Parse for Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exists to fix nested routes defined in a separate component in erased mode,
|
||||
/// by replacing the return type with AnyNestedRoute, which is what it'll be, but is required as the return type for compiler inference.
|
||||
fn maybe_modify_return_type(ret: &mut ReturnType) {
|
||||
#[cfg(feature = "__internal_erase_components")]
|
||||
{
|
||||
if let ReturnType::Type(_, ty) = ret {
|
||||
if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = ty.as_ref() {
|
||||
// If one of the bounds is MatchNestedRoutes, we need to replace the return type with AnyNestedRoute:
|
||||
if bounds.iter().any(|bound| {
|
||||
if let syn::TypeParamBound::Trait(trait_bound) = bound {
|
||||
if trait_bound.path.segments.iter().any(
|
||||
|path_segment| {
|
||||
path_segment.ident == "MatchNestedRoutes"
|
||||
},
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}) {
|
||||
*ty = parse_quote!(
|
||||
::leptos_router::any_nested_route::AnyNestedRoute
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "__internal_erase_components"))]
|
||||
{
|
||||
let _ = ret;
|
||||
}
|
||||
}
|
||||
|
||||
// implemented manually because Vec::drain_filter is nightly only
|
||||
// follows std recommended parallel
|
||||
pub fn drain_filter<T>(
|
||||
@@ -296,9 +331,9 @@ impl ToTokens for Model {
|
||||
|
||||
let component = if *is_transparent {
|
||||
body_expr
|
||||
} else if cfg!(erase_components) {
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
quote! {
|
||||
::leptos::prelude::IntoAny::into_any(
|
||||
::leptos::prelude::IntoMaybeErased::into_maybe_erased(
|
||||
::leptos::reactive::graph::untrack_with_diagnostics(
|
||||
move || {
|
||||
#tracing_guard_expr
|
||||
@@ -613,7 +648,8 @@ impl Parse for DummyModel {
|
||||
drain_filter(&mut attrs, |attr| !attr.path().is_ident("doc"));
|
||||
|
||||
let vis: Visibility = input.parse()?;
|
||||
let sig: Signature = input.parse()?;
|
||||
let mut sig: Signature = input.parse()?;
|
||||
maybe_modify_return_type(&mut sig.output);
|
||||
|
||||
// The body is left untouched, so it will not cause an error
|
||||
// even if the syntax is invalid.
|
||||
|
||||
@@ -281,7 +281,11 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
#[proc_macro]
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
view_macro_impl(tokens, true)
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
view(tokens)
|
||||
} else {
|
||||
view_macro_impl(tokens, true)
|
||||
}
|
||||
}
|
||||
|
||||
fn view_macro_impl(tokens: TokenStream, template: bool) -> TokenStream {
|
||||
@@ -923,7 +927,7 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
args.into(),
|
||||
s.into(),
|
||||
Some(syn::parse_quote!(::leptos::server_fn)),
|
||||
"/api",
|
||||
option_env!("SERVER_FN_PREFIX").unwrap_or("/api"),
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
|
||||
@@ -170,8 +170,14 @@ pub(crate) fn component_to_tokens(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let spreads = (!(spreads.is_empty())).then(|| {
|
||||
quote! {
|
||||
.add_any_attr((#(#spreads,)*).into_attr())
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
quote! {
|
||||
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.add_any_attr((#(#spreads,)*))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -428,6 +428,12 @@ fn element_children_to_tokens(
|
||||
{ #child }
|
||||
)
|
||||
})
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
.child(
|
||||
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
|
||||
)
|
||||
})
|
||||
} else if children.len() > 16 {
|
||||
// implementations of various traits used in routing and rendering are implemented for
|
||||
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
|
||||
@@ -473,6 +479,10 @@ fn fragment_to_tokens(
|
||||
None
|
||||
} else if children.len() == 1 {
|
||||
children.into_iter().next()
|
||||
} else if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
leptos::tachys::view::iterators::StaticVec::from(vec![#(#children.into_maybe_erased()),*])
|
||||
})
|
||||
} else if children.len() > 16 {
|
||||
// implementations of various traits used in routing and rendering are implemented for
|
||||
// tuples of sizes 0, 1, 2, 3, ... N. N varies but is > 16. The traits are also implemented
|
||||
@@ -757,10 +767,18 @@ pub(crate) fn element_to_tokens(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(quote! {
|
||||
(#(#attributes,)*)
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
|
||||
if cfg!(feature = "__internal_erase_components") {
|
||||
Some(quote! {
|
||||
vec![#(#attributes.into_any_attr(),)*]
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
} else {
|
||||
Some(quote! {
|
||||
(#(#attributes,)*)
|
||||
#(.add_any_attr(#additions))*
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let tag = name.to_string();
|
||||
// collect close_tag name to emit semantic information for IDE.
|
||||
|
||||
@@ -83,7 +83,7 @@ pub(crate) fn slot_to_tokens(
|
||||
let value = attr.value().map(|v| {
|
||||
quote! { #v }
|
||||
})?;
|
||||
Some(quote! { (#name, ::leptos::IntoAttribute::into_attribute(#value)) })
|
||||
Some(quote! { (#name, #value) })
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub mod tests {
|
||||
|
||||
use leptos::{
|
||||
server,
|
||||
server_fn::{codec, ServerFn, ServerFnError},
|
||||
server_fn::{codec, Http, ServerFn, ServerFnError},
|
||||
};
|
||||
use std::any::TypeId;
|
||||
|
||||
@@ -19,8 +18,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,8 +31,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::Cbor>()
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,8 +44,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::Cbor>()
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::Cbor, codec::Cbor>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,8 +57,8 @@ pub mod tests {
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,8 +73,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<FooBar as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,8 +90,8 @@ pub mod tests {
|
||||
"/foo/bar/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,8 +107,8 @@ pub mod tests {
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::GetUrl>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::GetUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,8 +123,8 @@ pub mod tests {
|
||||
"/api/path/to/my/endpoint"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
TypeId::of::<<MyServerAction as ServerFn>::Protocol>(),
|
||||
TypeId::of::<Http<codec::PostUrl, codec::Json>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(not(erase_components))]
|
||||
#[cfg(not(feature = "__internal_erase_components"))]
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
version = { workspace = true }
|
||||
# TODO revert to { workspace = true } before 0.8.0 release
|
||||
# this is a hack because I missing bumping the hydration_context version number before publishing
|
||||
version = "0.8.0-alpha2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -3,7 +3,7 @@ use reactive_graph::{
|
||||
owner::use_context,
|
||||
traits::DefinedAt,
|
||||
};
|
||||
use server_fn::{error::ServerFnErrorSerde, ServerFn, ServerFnError};
|
||||
use server_fn::{error::FromServerFnError, ServerFn};
|
||||
use std::{ops::Deref, panic::Location, sync::Arc};
|
||||
|
||||
/// An error that can be caused by a server action.
|
||||
@@ -42,7 +42,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
inner: ArcAction<S, Result<S::Output, S::Error>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -52,13 +52,14 @@ where
|
||||
S: ServerFn + Clone + Send + Sync + 'static,
|
||||
S::Output: Send + Sync + 'static,
|
||||
S::Error: Send + Sync + 'static,
|
||||
S::Error: FromServerFnError,
|
||||
{
|
||||
/// Creates a new [`ArcAction`] that will call the server function `S` when dispatched.
|
||||
#[track_caller]
|
||||
pub fn new() -> Self {
|
||||
let err = use_context::<ServerActionError>().and_then(|error| {
|
||||
(error.path() == S::PATH)
|
||||
.then(|| ServerFnError::<S::Error>::de(error.err()))
|
||||
.then(|| S::Error::de(error.err()))
|
||||
.map(Err)
|
||||
});
|
||||
Self {
|
||||
@@ -76,7 +77,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
type Target = ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
type Target = ArcAction<S, Result<S::Output, S::Error>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@@ -131,7 +132,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
inner: Action<S, Result<S::Output, S::Error>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -146,7 +147,7 @@ where
|
||||
pub fn new() -> Self {
|
||||
let err = use_context::<ServerActionError>().and_then(|error| {
|
||||
(error.path() == S::PATH)
|
||||
.then(|| ServerFnError::<S::Error>::de(error.err()))
|
||||
.then(|| S::Error::de(error.err()))
|
||||
.map(Err)
|
||||
});
|
||||
Self {
|
||||
@@ -182,15 +183,14 @@ where
|
||||
S::Output: Send + Sync + 'static,
|
||||
S::Error: Send + Sync + 'static,
|
||||
{
|
||||
type Target = Action<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
type Target = Action<S, Result<S::Output, S::Error>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> From<ServerAction<S>>
|
||||
for Action<S, Result<S::Output, ServerFnError<S::Error>>>
|
||||
impl<S> From<ServerAction<S>> for Action<S, Result<S::Output, S::Error>>
|
||||
where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
|
||||
@@ -79,7 +79,7 @@ mod view_implementations {
|
||||
use reactive_graph::traits::Read;
|
||||
use std::future::Future;
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
html::attribute::{any_attribute::AnyAttribute, Attribute},
|
||||
hydration::Cursor,
|
||||
reactive_graph::{RenderEffectState, Suspend, SuspendState},
|
||||
ssr::StreamBuilder,
|
||||
@@ -135,6 +135,7 @@ mod view_implementations {
|
||||
Ser: Send + 'static,
|
||||
{
|
||||
type AsyncOutput = Option<T>;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -152,12 +153,14 @@ mod view_implementations {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
(move || Suspend::new(async move { self.await })).to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,6 +170,7 @@ mod view_implementations {
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -176,6 +180,7 @@ mod view_implementations {
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,5 +192,9 @@ mod view_implementations {
|
||||
(move || Suspend::new(async move { self.await }))
|
||||
.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,12 @@ where
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
panic!(
|
||||
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||
will cause the response to hang, because LocalResources are \
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
@@ -358,6 +364,12 @@ where
|
||||
fn try_read_untracked(&self) -> Option<Self::Value> {
|
||||
if let Some(mut notifier) = use_context::<LocalResourceNotifier>() {
|
||||
notifier.notify();
|
||||
} else if cfg!(feature = "ssr") {
|
||||
panic!(
|
||||
"Reading from a LocalResource outside Suspense in `ssr` mode \
|
||||
will cause the response to hang, because LocalResources are \
|
||||
always pending on the server."
|
||||
);
|
||||
}
|
||||
self.data.try_read_untracked()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use reactive_graph::{
|
||||
actions::{ArcMultiAction, MultiAction},
|
||||
traits::DefinedAt,
|
||||
};
|
||||
use server_fn::{ServerFn, ServerFnError};
|
||||
use server_fn::ServerFn;
|
||||
use std::{ops::Deref, panic::Location};
|
||||
|
||||
/// An [`ArcMultiAction`] that can be used to call a server function.
|
||||
@@ -11,7 +11,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
inner: ArcMultiAction<S, Result<S::Output, S::Error>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
@@ -40,7 +40,7 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
type Target = ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
type Target = ArcMultiAction<S, Result<S::Output, S::Error>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@@ -95,13 +95,13 @@ where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
inner: MultiAction<S, Result<S::Output, S::Error>>,
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<S> From<ServerMultiAction<S>>
|
||||
for MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
|
||||
for MultiAction<S, Result<S::Output, S::Error>>
|
||||
where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
@@ -152,7 +152,7 @@ where
|
||||
S::Output: 'static,
|
||||
S::Error: 'static,
|
||||
{
|
||||
type Target = MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
|
||||
type Target = MultiAction<S, Result<S::Output, S::Error>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.7.7"
|
||||
version = "0.8.0-alpha"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::NextAttribute,
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
component, html,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
@@ -103,6 +103,7 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = BodyView<At::AsyncOutput>;
|
||||
type Owned = BodyView<At::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = At::MIN_LENGTH;
|
||||
|
||||
@@ -122,10 +123,14 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
_ = html::attributes_to_html(self.attributes, &mut buf);
|
||||
_ = html::attributes_to_html(
|
||||
(self.attributes, extra_attrs),
|
||||
&mut buf,
|
||||
);
|
||||
if !buf.is_empty() {
|
||||
_ = meta.body.send(buf);
|
||||
}
|
||||
@@ -142,6 +147,12 @@ where
|
||||
|
||||
BodyViewState { attributes }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
BodyView {
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> Mountable for BodyViewState<At>
|
||||
@@ -160,4 +171,11 @@ where
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![document()
|
||||
.body()
|
||||
.expect("there to be a <body> element")
|
||||
.into()]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::ServerMetaContext;
|
||||
use leptos::{
|
||||
attr::NextAttribute,
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
component, html,
|
||||
reactive::owner::use_context,
|
||||
tachys::{
|
||||
@@ -103,6 +103,7 @@ where
|
||||
At: Attribute,
|
||||
{
|
||||
type AsyncOutput = HtmlView<At::AsyncOutput>;
|
||||
type Owned = HtmlView<At::CloneableOwned>;
|
||||
|
||||
const MIN_LENGTH: usize = At::MIN_LENGTH;
|
||||
|
||||
@@ -122,10 +123,14 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||
let mut buf = String::new();
|
||||
_ = html::attributes_to_html(self.attributes, &mut buf);
|
||||
_ = html::attributes_to_html(
|
||||
(self.attributes, extra_attrs),
|
||||
&mut buf,
|
||||
);
|
||||
if !buf.is_empty() {
|
||||
_ = meta.html.send(buf);
|
||||
}
|
||||
@@ -145,6 +150,12 @@ where
|
||||
|
||||
HtmlViewState { attributes }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
HtmlView {
|
||||
attributes: self.attributes.into_cloneable_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<At> Mountable for HtmlViewState<At>
|
||||
@@ -165,4 +176,10 @@ where
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![document()
|
||||
.document_element()
|
||||
.expect("there to be a <html> element")]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
use futures::{Stream, StreamExt};
|
||||
use leptos::{
|
||||
attr::NextAttribute,
|
||||
attr::{any_attribute::AnyAttribute, NextAttribute},
|
||||
component,
|
||||
logging::debug_warn,
|
||||
oco::Oco,
|
||||
@@ -405,6 +405,7 @@ where
|
||||
Ch: RenderHtml + Send,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = RegisteredMetaTag<E, At::CloneableOwned, Ch::Owned>;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -422,6 +423,7 @@ where
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// meta tags are rendered into the buffer stored into the context
|
||||
// the value has already been taken out, when we're on the server
|
||||
@@ -433,6 +435,7 @@ where
|
||||
&mut Position::NextChild,
|
||||
false,
|
||||
false,
|
||||
vec![],
|
||||
);
|
||||
_ = cx.elements.send(buf); // fails only if the receiver is already dropped
|
||||
} else {
|
||||
@@ -464,6 +467,12 @@ where
|
||||
);
|
||||
RegisteredMetaTagState { state }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
RegisteredMetaTag {
|
||||
el: self.el.into_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, At, Ch> Mountable for RegisteredMetaTagState<E, At, Ch>
|
||||
@@ -496,6 +505,10 @@ where
|
||||
// we intended!
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
self.state.elements()
|
||||
}
|
||||
}
|
||||
|
||||
/// During server rendering, inserts the meta tags that have been generated by the other components
|
||||
@@ -537,6 +550,7 @@ impl AddAnyAttr for MetaTagsView {
|
||||
|
||||
impl RenderHtml for MetaTagsView {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -552,6 +566,7 @@ impl RenderHtml for MetaTagsView {
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
buf.push_str("<!--HEAD-->");
|
||||
}
|
||||
@@ -562,6 +577,10 @@ impl RenderHtml for MetaTagsView {
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait OrDefaultNonce {
|
||||
|
||||
@@ -36,6 +36,10 @@ pub fn Stylesheet(
|
||||
}
|
||||
|
||||
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document head that loads a `cargo-leptos`-hashed stylesheet.
|
||||
///
|
||||
/// This should only be used in the application’s server-side `shell` function, as
|
||||
/// [`LeptosOptions`] is not available in the browser. Unlike other `leptos_meta` components, it
|
||||
/// will render the `<link>` it creates exactly where it is called.
|
||||
#[component]
|
||||
pub fn HashedStylesheet(
|
||||
/// Leptos options
|
||||
@@ -74,11 +78,9 @@ pub fn HashedStylesheet(
|
||||
css_file_name.push_str(".css");
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let root = root.unwrap_or_default();
|
||||
// TODO additional attributes
|
||||
register(
|
||||
link()
|
||||
.id(id)
|
||||
.rel("stylesheet")
|
||||
.href(format!("{root}/{pkg_path}/{css_file_name}")),
|
||||
)
|
||||
|
||||
link()
|
||||
.id(id)
|
||||
.rel("stylesheet")
|
||||
.href(format!("{root}/{pkg_path}/{css_file_name}"))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{use_head, MetaContext, ServerMetaContext};
|
||||
use leptos::{
|
||||
attr::Attribute,
|
||||
attr::{any_attribute::AnyAttribute, Attribute},
|
||||
component,
|
||||
oco::Oco,
|
||||
reactive::{
|
||||
@@ -234,6 +234,7 @@ impl AddAnyAttr for TitleView {
|
||||
|
||||
impl RenderHtml for TitleView {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
@@ -249,6 +250,7 @@ impl RenderHtml for TitleView {
|
||||
_position: &mut Position,
|
||||
_escape: bool,
|
||||
_mark_branches: bool,
|
||||
_extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// meta tags are rendered into the buffer stored into the context
|
||||
// the value has already been taken out, when we're on the server
|
||||
@@ -282,6 +284,10 @@ impl RenderHtml for TitleView {
|
||||
});
|
||||
TitleViewState { effect }
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable for TitleViewState {
|
||||
@@ -299,4 +305,8 @@ impl Mountable for TitleViewState {
|
||||
fn insert_before_this(&self, _child: &mut dyn Mountable) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<leptos::tachys::renderer::types::Element> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0-alpha2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -961,11 +961,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O, S> Action<I, O, S>
|
||||
impl<I, O> Action<I, O>
|
||||
where
|
||||
I: Send + Sync + 'static,
|
||||
O: Send + Sync + 'static,
|
||||
S: Storage<ArcAction<I, O>>,
|
||||
{
|
||||
/// Creates a new action, which does not require the action itself to be `Send`, but will run
|
||||
/// it on the same thread it was created on.
|
||||
@@ -1006,6 +1005,56 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> Action<I, O, LocalStorage>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
/// Creates a new action, which neither requires the action itself nor the
|
||||
/// value it returns to be `Send`. If this action is accessed from outside the
|
||||
/// thread on which it was created, it panics.
|
||||
///
|
||||
/// This combines the features of [`Action::new_local`] and [`Action::new_unsync`].
|
||||
#[track_caller]
|
||||
pub fn new_unsync_local<F, Fu>(action_fn: F) -> Self
|
||||
where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: ArenaItem::new_with_storage(ArcAction::new_unsync(
|
||||
action_fn,
|
||||
)),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new action, which neither requires the action itself nor the
|
||||
/// value it returns to be `Send`, and provides it with an initial value.
|
||||
/// If this action is accessed from outside the thread on which it was created, it panics.
|
||||
///
|
||||
/// This combines the features of [`Action::new_local_with_value`] and
|
||||
/// [`Action::new_unsync_with_value`].
|
||||
#[track_caller]
|
||||
pub fn new_unsync_local_with_value<F, Fu>(
|
||||
value: Option<O>,
|
||||
action_fn: F,
|
||||
) -> Self
|
||||
where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Self {
|
||||
inner: ArenaItem::new_with_storage(
|
||||
ArcAction::new_unsync_with_value(value, action_fn),
|
||||
),
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O, S> DefinedAt for Action<I, O, S> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
sync::{Arc, RwLock},
|
||||
sync::{Arc, RwLock, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
pub struct MemoInner<T, S>
|
||||
@@ -72,17 +72,21 @@ where
|
||||
}
|
||||
|
||||
fn mark_check(&self) {
|
||||
{
|
||||
let mut lock = self.reactivity.write().or_poisoned();
|
||||
if lock.state != ReactiveNodeState::Dirty {
|
||||
lock.state = ReactiveNodeState::Check;
|
||||
/// codegen optimisation:
|
||||
fn inner(reactivity: &RwLock<MemoInnerReactivity>) {
|
||||
{
|
||||
let mut lock = reactivity.write().or_poisoned();
|
||||
if lock.state != ReactiveNodeState::Dirty {
|
||||
lock.state = ReactiveNodeState::Check;
|
||||
}
|
||||
}
|
||||
for sub in
|
||||
(&reactivity.read().or_poisoned().subscribers).into_iter()
|
||||
{
|
||||
sub.mark_check();
|
||||
}
|
||||
}
|
||||
for sub in
|
||||
(&self.reactivity.read().or_poisoned().subscribers).into_iter()
|
||||
{
|
||||
sub.mark_check();
|
||||
}
|
||||
inner(&self.reactivity);
|
||||
}
|
||||
|
||||
fn mark_subscribers_check(&self) {
|
||||
@@ -93,64 +97,87 @@ where
|
||||
}
|
||||
|
||||
fn update_if_necessary(&self) -> bool {
|
||||
let (state, sources) = {
|
||||
let inner = self.reactivity.read().or_poisoned();
|
||||
(inner.state, inner.sources.clone())
|
||||
};
|
||||
/// codegen optimisation:
|
||||
fn needs_update(reactivity: &RwLock<MemoInnerReactivity>) -> bool {
|
||||
let (state, sources) = {
|
||||
let inner = reactivity.read().or_poisoned();
|
||||
(inner.state, inner.sources.clone())
|
||||
};
|
||||
match state {
|
||||
ReactiveNodeState::Clean => false,
|
||||
ReactiveNodeState::Dirty => true,
|
||||
ReactiveNodeState::Check => {
|
||||
(&sources).into_iter().any(|source| {
|
||||
source.update_if_necessary()
|
||||
|| reactivity.read().or_poisoned().state
|
||||
== ReactiveNodeState::Dirty
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let needs_update = match state {
|
||||
ReactiveNodeState::Clean => false,
|
||||
ReactiveNodeState::Dirty => true,
|
||||
ReactiveNodeState::Check => (&sources).into_iter().any(|source| {
|
||||
source.update_if_necessary()
|
||||
|| self.reactivity.read().or_poisoned().state
|
||||
== ReactiveNodeState::Dirty
|
||||
}),
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
let fun = self.fun.clone();
|
||||
let owner = self.owner.clone();
|
||||
if needs_update(&self.reactivity) {
|
||||
// No deadlock risk, because we only hold the value lock.
|
||||
let value = self.value.write().or_poisoned().take();
|
||||
|
||||
let any_subscriber =
|
||||
{ self.reactivity.read().or_poisoned().any_subscriber.clone() };
|
||||
any_subscriber.clear_sources(&any_subscriber);
|
||||
let (new_value, changed) = owner.with_cleanup(|| {
|
||||
/// codegen optimisation:
|
||||
fn inner_1(
|
||||
reactivity: &RwLock<MemoInnerReactivity>,
|
||||
) -> AnySubscriber {
|
||||
let any_subscriber =
|
||||
reactivity.read().or_poisoned().any_subscriber.clone();
|
||||
any_subscriber.clear_sources(&any_subscriber);
|
||||
any_subscriber
|
||||
.with_observer(|| fun(value.map(StorageAccess::into_taken)))
|
||||
}
|
||||
let any_subscriber = inner_1(&self.reactivity);
|
||||
|
||||
let (new_value, changed) = self.owner.with_cleanup(|| {
|
||||
any_subscriber.with_observer(|| {
|
||||
(self.fun)(value.map(StorageAccess::into_taken))
|
||||
})
|
||||
});
|
||||
|
||||
// Two locks are aquired, so order matters.
|
||||
let mut reactivity_lock = self.reactivity.write().or_poisoned();
|
||||
let reactivity_lock = self.reactivity.write().or_poisoned();
|
||||
{
|
||||
// Safety: Can block endlessly if the user is has a ReadGuard on the value
|
||||
let mut value_lock = self.value.write().or_poisoned();
|
||||
*value_lock = Some(S::wrap(new_value));
|
||||
}
|
||||
reactivity_lock.state = ReactiveNodeState::Clean;
|
||||
|
||||
if changed {
|
||||
let subs = reactivity_lock.subscribers.clone();
|
||||
drop(reactivity_lock);
|
||||
for sub in subs {
|
||||
// don't trigger reruns of effects/memos
|
||||
// basically: if one of the observers has triggered this memo to
|
||||
// run, it doesn't need to be re-triggered because of this change
|
||||
if !Observer::is(&sub) {
|
||||
sub.mark_dirty();
|
||||
/// codegen optimisation:
|
||||
fn inner_2(
|
||||
changed: bool,
|
||||
mut reactivity_lock: RwLockWriteGuard<'_, MemoInnerReactivity>,
|
||||
) {
|
||||
reactivity_lock.state = ReactiveNodeState::Clean;
|
||||
|
||||
if changed {
|
||||
let subs = reactivity_lock.subscribers.clone();
|
||||
drop(reactivity_lock);
|
||||
for sub in subs {
|
||||
// don't trigger reruns of effects/memos
|
||||
// basically: if one of the observers has triggered this memo to
|
||||
// run, it doesn't need to be re-triggered because of this change
|
||||
if !Observer::is(&sub) {
|
||||
sub.mark_dirty();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
drop(reactivity_lock);
|
||||
}
|
||||
} else {
|
||||
drop(reactivity_lock);
|
||||
}
|
||||
inner_2(changed, reactivity_lock);
|
||||
|
||||
changed
|
||||
} else {
|
||||
let mut lock = self.reactivity.write().or_poisoned();
|
||||
lock.state = ReactiveNodeState::Clean;
|
||||
false
|
||||
/// codegen optimisation:
|
||||
fn inner(reactivity: &RwLock<MemoInnerReactivity>) -> bool {
|
||||
let mut lock = reactivity.write().or_poisoned();
|
||||
lock.state = ReactiveNodeState::Clean;
|
||||
false
|
||||
}
|
||||
inner(&self.reactivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
},
|
||||
owner::Owner,
|
||||
};
|
||||
use any_spawner::Executor;
|
||||
use futures::StreamExt;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
@@ -54,7 +53,7 @@ where
|
||||
{
|
||||
/// Creates a new render effect, which immediately runs `fun`.
|
||||
pub fn new(fun: impl FnMut(Option<T>) -> T + 'static) -> Self {
|
||||
Self::new_with_value(fun, None)
|
||||
Self::new_with_value_erased(Box::new(fun), None)
|
||||
}
|
||||
|
||||
/// Creates a new render effect with an initial value.
|
||||
@@ -62,59 +61,70 @@ where
|
||||
fun: impl FnMut(Option<T>) -> T + 'static,
|
||||
initial_value: Option<T>,
|
||||
) -> Self {
|
||||
fn erased<T>(
|
||||
mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
|
||||
initial_value: Option<T>,
|
||||
) -> RenderEffect<T> {
|
||||
let (observer, mut rx) = channel();
|
||||
let value = Arc::new(RwLock::new(None::<T>));
|
||||
Self::new_with_value_erased(Box::new(fun), initial_value)
|
||||
}
|
||||
|
||||
fn new_with_value_erased(
|
||||
mut fun: Box<dyn FnMut(Option<T>) -> T + 'static>,
|
||||
initial_value: Option<T>,
|
||||
) -> Self {
|
||||
// codegen optimisation:
|
||||
fn prep() -> (Owner, Arc<RwLock<EffectInner>>, crate::channel::Receiver)
|
||||
{
|
||||
let (observer, rx) = channel();
|
||||
let owner = Owner::new();
|
||||
let inner = Arc::new(RwLock::new(EffectInner {
|
||||
dirty: false,
|
||||
observer,
|
||||
sources: SourceSet::new(),
|
||||
}));
|
||||
|
||||
let initial_value = cfg!(feature = "effects").then(|| {
|
||||
owner.with(|| {
|
||||
inner
|
||||
.to_any_subscriber()
|
||||
.with_observer(|| fun(initial_value))
|
||||
})
|
||||
});
|
||||
*value.write().or_poisoned() = initial_value;
|
||||
|
||||
if cfg!(feature = "effects") {
|
||||
Executor::spawn_local({
|
||||
let value = Arc::clone(&value);
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if !owner.paused()
|
||||
&& subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
let old_value = mem::take(
|
||||
&mut *value.write().or_poisoned(),
|
||||
);
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun(old_value))
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RenderEffect { value, inner }
|
||||
(owner, inner, rx)
|
||||
}
|
||||
|
||||
erased(Box::new(fun), initial_value)
|
||||
let (owner, inner, mut rx) = prep();
|
||||
|
||||
let value = Arc::new(RwLock::new(None::<T>));
|
||||
|
||||
#[cfg(not(feature = "effects"))]
|
||||
{
|
||||
let _ = initial_value;
|
||||
let _ = owner;
|
||||
let _ = &mut rx;
|
||||
let _ = &mut fun;
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
{
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
*value.write().or_poisoned() = Some(
|
||||
owner.with(|| subscriber.with_observer(|| fun(initial_value))),
|
||||
);
|
||||
|
||||
any_spawner::Executor::spawn_local({
|
||||
let value = Arc::clone(&value);
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if !owner.paused()
|
||||
&& subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
})
|
||||
{
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| fun(old_value))
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RenderEffect { value, inner }
|
||||
}
|
||||
|
||||
/// Mutably accesses the current value.
|
||||
|
||||
@@ -207,15 +207,26 @@ impl Owner {
|
||||
|
||||
/// Runs the given function with this as the current `Owner`.
|
||||
pub fn with<T>(&self, fun: impl FnOnce() -> T) -> T {
|
||||
let prev = {
|
||||
OWNER.with(|o| Option::replace(&mut *o.borrow_mut(), self.clone()))
|
||||
};
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
Arena::set(&self.inner.read().or_poisoned().arena);
|
||||
// codegen optimisation:
|
||||
fn inner_1(self_: &Owner) -> Option<Owner> {
|
||||
let prev =
|
||||
{ OWNER.with(|o| (*o.borrow_mut()).replace(self_.clone())) };
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
Arena::set(&self_.inner.read().or_poisoned().arena);
|
||||
prev
|
||||
}
|
||||
let prev = inner_1(self);
|
||||
|
||||
let val = fun();
|
||||
OWNER.with(|o| {
|
||||
*o.borrow_mut() = prev;
|
||||
});
|
||||
|
||||
// monomorphisation optimisation:
|
||||
fn inner_2(prev: Option<Owner>) {
|
||||
OWNER.with(|o| {
|
||||
*o.borrow_mut() = prev;
|
||||
});
|
||||
}
|
||||
inner_2(prev);
|
||||
|
||||
val
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0-alpha"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -100,9 +100,6 @@ where
|
||||
|
||||
let mut full_path = self.path().into_iter().collect::<StorePath>();
|
||||
full_path.pop();
|
||||
|
||||
// build a list of triggers, starting with the full path to this node and ending with the root
|
||||
// this will mean that the root is the final item, and this path is first
|
||||
let mut triggers = Vec::with_capacity(full_path.len());
|
||||
triggers.push(trigger.this.clone());
|
||||
loop {
|
||||
@@ -113,17 +110,6 @@ where
|
||||
}
|
||||
full_path.pop();
|
||||
}
|
||||
|
||||
// when the WriteGuard is dropped, each trigger will be notified, in order
|
||||
// reversing the list will cause the triggers to be notified starting from the root,
|
||||
// then to each child down to this one
|
||||
//
|
||||
// notifying from the root down is important for things like OptionStoreExt::map()/unwrap(),
|
||||
// where it's really important that any effects that subscribe to .is_some() run before effects
|
||||
// that subscribe to the inner value, so that the inner effect can be canceled if the outer switches to `None`
|
||||
// (see https://github.com/leptos-rs/leptos/issues/3704)
|
||||
triggers.reverse();
|
||||
|
||||
let guard = WriteGuard::new(triggers, parent);
|
||||
|
||||
Some(MappedMut::new(guard, self.read, self.write))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.1.7"
|
||||
version = "0.2.0-alpha"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.7.7"
|
||||
version = "0.8.0-alpha"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -66,3 +66,9 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(erase_components)',
|
||||
] }
|
||||
|
||||
@@ -11,7 +11,8 @@ use crate::{
|
||||
navigate::NavigateOptions,
|
||||
nested_router::NestedRoutesView,
|
||||
resolve_path::resolve_path,
|
||||
ChooseView, MatchNestedRoutes, NestedRoute, RouteDefs, SsrMode,
|
||||
ChooseView, MatchNestedRoutes, NestedRoute, PossibleRouteMatch, RouteDefs,
|
||||
SsrMode,
|
||||
};
|
||||
use either_of::EitherOf3;
|
||||
use leptos::{children, prelude::*};
|
||||
@@ -28,7 +29,6 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tachys::view::any_view::AnyView;
|
||||
|
||||
/// A wrapper that allows passing route definitions as children to a component like [`Routes`],
|
||||
/// [`FlatRoutes`], [`ParentRoute`], or [`ProtectedParentRoute`].
|
||||
@@ -344,11 +344,14 @@ pub fn Route<Segments, View>(
|
||||
/// Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
) -> NestedRoute<Segments, (), (), View>
|
||||
) -> <NestedRoute<Segments, (), (), View> as IntoMaybeErased>::Output
|
||||
where
|
||||
View: ChooseView,
|
||||
View: ChooseView + Clone + 'static,
|
||||
Segments: PossibleRouteMatch + Clone + Send + 'static,
|
||||
{
|
||||
NestedRoute::new(path, view).ssr_mode(ssr)
|
||||
NestedRoute::new(path, view)
|
||||
.ssr_mode(ssr)
|
||||
.into_maybe_erased()
|
||||
}
|
||||
|
||||
/// Describes a portion of the nested layout of the app, specifying the route it should match
|
||||
@@ -368,146 +371,186 @@ pub fn ParentRoute<Segments, View, Children>(
|
||||
/// Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
) -> NestedRoute<Segments, Children, (), View>
|
||||
) -> <NestedRoute<Segments, Children, (), View> as IntoMaybeErased>::Output
|
||||
where
|
||||
View: ChooseView,
|
||||
View: ChooseView + Clone + 'static,
|
||||
Children: MatchNestedRoutes + Send + Clone + 'static,
|
||||
Segments: PossibleRouteMatch + Clone + Send + 'static,
|
||||
{
|
||||
let children = children.into_inner();
|
||||
NestedRoute::new(path, view).ssr_mode(ssr).child(children)
|
||||
NestedRoute::new(path, view)
|
||||
.ssr_mode(ssr)
|
||||
.child(children)
|
||||
.into_maybe_erased()
|
||||
}
|
||||
|
||||
/// Describes a route that is guarded by a certain condition. This works the same way as
|
||||
/// [`<Route/>`], except that if the `condition` function evaluates to `Some(false)`, it
|
||||
/// redirects to `redirect_path` instead of displaying its `view`.
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
|
||||
/// The path fragment that this route should match. This can be created using the
|
||||
/// [`path`](crate::path) macro, or path segments ([`StaticSegment`](crate::StaticSegment),
|
||||
/// [`ParamSegment`](crate::ParamSegment), [`WildcardSegment`](crate::WildcardSegment), and
|
||||
/// [`OptionalParamSegment`](crate::OptionalParamSegment)).
|
||||
path: Segments,
|
||||
/// The view for this route.
|
||||
view: ViewFn,
|
||||
/// A function that returns `Option<bool>`, where `Some(true)` means that the user can access
|
||||
/// the page, `Some(false)` means the user cannot access the page, and `None` means this
|
||||
/// information is still loading.
|
||||
condition: C,
|
||||
/// The path that will be redirected to if the condition is `Some(false)`.
|
||||
redirect_path: PathFn,
|
||||
/// Will be displayed while the condition is pending. By default this is the empty view.
|
||||
#[prop(optional, into)]
|
||||
fallback: children::ViewFn,
|
||||
/// The mode that this route prefers during server-side rendering.
|
||||
/// Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
) -> NestedRoute<Segments, (), (), impl Fn() -> AnyView + Send + Clone>
|
||||
where
|
||||
ViewFn: Fn() -> View + Send + Clone + 'static,
|
||||
View: IntoView + 'static,
|
||||
C: Fn() -> Option<bool> + Send + Clone + 'static,
|
||||
PathFn: Fn() -> P + Send + Clone + 'static,
|
||||
P: Display + 'static,
|
||||
{
|
||||
let fallback = move || fallback.run();
|
||||
let view = move || {
|
||||
let condition = condition.clone();
|
||||
let redirect_path = redirect_path.clone();
|
||||
let view = view.clone();
|
||||
let fallback = fallback.clone();
|
||||
(view! {
|
||||
<Transition fallback=fallback.clone()>
|
||||
{move || {
|
||||
let condition = condition();
|
||||
let view = view.clone();
|
||||
let redirect_path = redirect_path.clone();
|
||||
let fallback = fallback.clone();
|
||||
Unsuspend::new(move || match condition {
|
||||
Some(true) => EitherOf3::A(view()),
|
||||
#[allow(clippy::unit_arg)]
|
||||
Some(false) => {
|
||||
EitherOf3::B(view! { <Redirect path=redirect_path()/> }.into_inner())
|
||||
}
|
||||
None => EitherOf3::C(fallback()),
|
||||
})
|
||||
}}
|
||||
|
||||
</Transition>
|
||||
})
|
||||
.into_any()
|
||||
};
|
||||
NestedRoute::new(path, view).ssr_mode(ssr)
|
||||
}
|
||||
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedParentRoute<Segments, ViewFn, View, C, PathFn, P, Children>(
|
||||
/// The path fragment that this route should match. This can be created using the
|
||||
/// [`path`](crate::path) macro, or path segments ([`StaticSegment`](crate::StaticSegment),
|
||||
/// [`ParamSegment`](crate::ParamSegment), [`WildcardSegment`](crate::WildcardSegment), and
|
||||
/// [`OptionalParamSegment`](crate::OptionalParamSegment)).
|
||||
path: Segments,
|
||||
/// The view for this route.
|
||||
view: ViewFn,
|
||||
/// A function that returns `Option<bool>`, where `Some(true)` means that the user can access
|
||||
/// the page, `Some(false)` means the user cannot access the page, and `None` means this
|
||||
/// information is still loading.
|
||||
condition: C,
|
||||
/// Will be displayed while the condition is pending. By default this is the empty view.
|
||||
#[prop(optional, into)]
|
||||
fallback: children::ViewFn,
|
||||
/// The path that will be redirected to if the condition is `Some(false)`.
|
||||
redirect_path: PathFn,
|
||||
/// Nested child routes.
|
||||
children: RouteChildren<Children>,
|
||||
/// The mode that this route prefers during server-side rendering.
|
||||
/// Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
) -> NestedRoute<Segments, Children, (), impl Fn() -> AnyView + Send + Clone>
|
||||
where
|
||||
ViewFn: Fn() -> View + Send + Clone + 'static,
|
||||
View: IntoView + 'static,
|
||||
C: Fn() -> Option<bool> + Send + Clone + 'static,
|
||||
PathFn: Fn() -> P + Send + Clone + 'static,
|
||||
P: Display + 'static,
|
||||
{
|
||||
let fallback = move || fallback.run();
|
||||
let children = children.into_inner();
|
||||
let view = move || {
|
||||
let condition = condition.clone();
|
||||
let redirect_path = redirect_path.clone();
|
||||
let fallback = fallback.clone();
|
||||
let view = view.clone();
|
||||
let owner = Owner::current().unwrap();
|
||||
let view = {
|
||||
let fallback = fallback.clone();
|
||||
move || {
|
||||
let condition = condition();
|
||||
/// With the `impl Fn` in the return signature, IntoMaybeErased::Output isn't accepted by the compiler, so changing return type depending on the erasure flag.
|
||||
macro_rules! define_protected_route {
|
||||
($ret:ty) => {
|
||||
/// Describes a route that is guarded by a certain condition. This works the same way as
|
||||
/// [`<Route/>`], except that if the `condition` function evaluates to `Some(false)`, it
|
||||
/// redirects to `redirect_path` instead of displaying its `view`.
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedRoute<Segments, ViewFn, View, C, PathFn, P>(
|
||||
/// The path fragment that this route should match. This can be created using the
|
||||
/// [`path`](crate::path) macro, or path segments ([`StaticSegment`](crate::StaticSegment),
|
||||
/// [`ParamSegment`](crate::ParamSegment), [`WildcardSegment`](crate::WildcardSegment), and
|
||||
/// [`OptionalParamSegment`](crate::OptionalParamSegment)).
|
||||
path: Segments,
|
||||
/// The view for this route.
|
||||
view: ViewFn,
|
||||
/// A function that returns `Option<bool>`, where `Some(true)` means that the user can access
|
||||
/// the page, `Some(false)` means the user cannot access the page, and `None` means this
|
||||
/// information is still loading.
|
||||
condition: C,
|
||||
/// The path that will be redirected to if the condition is `Some(false)`.
|
||||
redirect_path: PathFn,
|
||||
/// Will be displayed while the condition is pending. By default this is the empty view.
|
||||
#[prop(optional, into)]
|
||||
fallback: children::ViewFn,
|
||||
/// The mode that this route prefers during server-side rendering.
|
||||
/// Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
) -> $ret
|
||||
where
|
||||
Segments: PossibleRouteMatch + Clone + Send + 'static,
|
||||
ViewFn: Fn() -> View + Send + Clone + 'static,
|
||||
View: IntoView + 'static,
|
||||
C: Fn() -> Option<bool> + Send + Clone + 'static,
|
||||
PathFn: Fn() -> P + Send + Clone + 'static,
|
||||
P: Display + 'static,
|
||||
{
|
||||
let fallback = move || fallback.run();
|
||||
let view = move || {
|
||||
let condition = condition.clone();
|
||||
let redirect_path = redirect_path.clone();
|
||||
let view = view.clone();
|
||||
let fallback = fallback.clone();
|
||||
(view! {
|
||||
<Transition fallback=fallback.clone()>
|
||||
{move || {
|
||||
let condition = condition();
|
||||
let view = view.clone();
|
||||
let redirect_path = redirect_path.clone();
|
||||
let fallback = fallback.clone();
|
||||
Unsuspend::new(move || match condition {
|
||||
Some(true) => EitherOf3::A(view()),
|
||||
#[allow(clippy::unit_arg)]
|
||||
Some(false) => {
|
||||
EitherOf3::B(view! { <Redirect path=redirect_path()/> }.into_inner())
|
||||
}
|
||||
None => EitherOf3::C(fallback()),
|
||||
})
|
||||
}}
|
||||
|
||||
</Transition>
|
||||
})
|
||||
.into_any()
|
||||
};
|
||||
NestedRoute::new(path, view).ssr_mode(ssr).into_maybe_erased()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(erase_components)]
|
||||
define_protected_route!(crate::any_nested_route::AnyNestedRoute);
|
||||
#[cfg(not(erase_components))]
|
||||
define_protected_route!(NestedRoute<Segments, (), (), impl Fn() -> AnyView + Send + Clone>);
|
||||
|
||||
/// With the `impl Fn` in the return signature, IntoMaybeErased::Output isn't accepted by the compiler, so changing return type depending on the erasure flag.
|
||||
macro_rules! define_protected_parent_route {
|
||||
($ret:ty) => {
|
||||
#[component(transparent)]
|
||||
pub fn ProtectedParentRoute<
|
||||
Segments,
|
||||
ViewFn,
|
||||
View,
|
||||
C,
|
||||
PathFn,
|
||||
P,
|
||||
Children,
|
||||
>(
|
||||
/// The path fragment that this route should match. This can be created using the
|
||||
/// [`path`](crate::path) macro, or path segments ([`StaticSegment`](crate::StaticSegment),
|
||||
/// [`ParamSegment`](crate::ParamSegment), [`WildcardSegment`](crate::WildcardSegment), and
|
||||
/// [`OptionalParamSegment`](crate::OptionalParamSegment)).
|
||||
path: Segments,
|
||||
/// The view for this route.
|
||||
view: ViewFn,
|
||||
/// A function that returns `Option<bool>`, where `Some(true)` means that the user can access
|
||||
/// the page, `Some(false)` means the user cannot access the page, and `None` means this
|
||||
/// information is still loading.
|
||||
condition: C,
|
||||
/// Will be displayed while the condition is pending. By default this is the empty view.
|
||||
#[prop(optional, into)]
|
||||
fallback: children::ViewFn,
|
||||
/// The path that will be redirected to if the condition is `Some(false)`.
|
||||
redirect_path: PathFn,
|
||||
/// Nested child routes.
|
||||
children: RouteChildren<Children>,
|
||||
/// The mode that this route prefers during server-side rendering.
|
||||
/// Defaults to out-of-order streaming.
|
||||
#[prop(optional)]
|
||||
ssr: SsrMode,
|
||||
) -> $ret
|
||||
where
|
||||
Segments: PossibleRouteMatch + Clone + Send + 'static,
|
||||
Children: MatchNestedRoutes + Send + Clone + 'static,
|
||||
ViewFn: Fn() -> View + Send + Clone + 'static,
|
||||
View: IntoView + 'static,
|
||||
C: Fn() -> Option<bool> + Send + Clone + 'static,
|
||||
PathFn: Fn() -> P + Send + Clone + 'static,
|
||||
P: Display + 'static,
|
||||
{
|
||||
let fallback = move || fallback.run();
|
||||
let children = children.into_inner();
|
||||
let view = move || {
|
||||
let condition = condition.clone();
|
||||
let redirect_path = redirect_path.clone();
|
||||
let fallback = fallback.clone();
|
||||
let owner = owner.clone();
|
||||
Unsuspend::new(move || match condition {
|
||||
// reset the owner so that things like providing context work
|
||||
// otherwise, this will be a child owner nested within the Transition, not
|
||||
// the parent owner of the Outlet
|
||||
//
|
||||
// clippy: not redundant, a FnOnce vs FnMut issue
|
||||
#[allow(clippy::redundant_closure)]
|
||||
Some(true) => EitherOf3::A(owner.with(|| view())),
|
||||
#[allow(clippy::unit_arg)]
|
||||
Some(false) => EitherOf3::B(
|
||||
view! { <Redirect path=redirect_path()/> }.into_inner(),
|
||||
),
|
||||
None => EitherOf3::C(fallback()),
|
||||
})
|
||||
}
|
||||
};
|
||||
(view! { <Transition fallback>{view}</Transition> }).into_any()
|
||||
let view = view.clone();
|
||||
let owner = Owner::current().unwrap();
|
||||
let view = {
|
||||
let fallback = fallback.clone();
|
||||
move || {
|
||||
let condition = condition();
|
||||
let view = view.clone();
|
||||
let redirect_path = redirect_path.clone();
|
||||
let fallback = fallback.clone();
|
||||
let owner = owner.clone();
|
||||
Unsuspend::new(move || match condition {
|
||||
// reset the owner so that things like providing context work
|
||||
// otherwise, this will be a child owner nested within the Transition, not
|
||||
// the parent owner of the Outlet
|
||||
//
|
||||
// clippy: not redundant, a FnOnce vs FnMut issue
|
||||
#[allow(clippy::redundant_closure)]
|
||||
Some(true) => EitherOf3::A(owner.with(|| view())),
|
||||
#[allow(clippy::unit_arg)]
|
||||
Some(false) => EitherOf3::B(
|
||||
view! { <Redirect path=redirect_path()/> }
|
||||
.into_inner(),
|
||||
),
|
||||
None => EitherOf3::C(fallback()),
|
||||
})
|
||||
}
|
||||
};
|
||||
(view! { <Transition fallback>{view}</Transition> }).into_any()
|
||||
};
|
||||
NestedRoute::new(path, view)
|
||||
.ssr_mode(ssr)
|
||||
.child(children)
|
||||
.into_maybe_erased()
|
||||
}
|
||||
};
|
||||
NestedRoute::new(path, view).ssr_mode(ssr).child(children)
|
||||
}
|
||||
|
||||
#[cfg(erase_components)]
|
||||
define_protected_parent_route!(crate::any_nested_route::AnyNestedRoute);
|
||||
#[cfg(not(erase_components))]
|
||||
define_protected_parent_route!(NestedRoute<Segments, Children, (), impl Fn() -> AnyView + Send + Clone>);
|
||||
|
||||
/// Redirects the user to a new URL, whether on the client side or on the server
|
||||
/// side. If rendered on the server, this sets a `302` status code and sets a `Location`
|
||||
/// header. If rendered in the browser, it uses client-side navigation to redirect.
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::{
|
||||
use any_spawner::Executor;
|
||||
use either_of::Either;
|
||||
use futures::FutureExt;
|
||||
use leptos::attr::{any_attribute::AnyAttribute, Attribute};
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, ScopedFuture},
|
||||
owner::{provide_context, Owner},
|
||||
@@ -26,7 +27,7 @@ use tachys::{
|
||||
view::{
|
||||
add_attr::AddAnyAttr,
|
||||
any_view::{AnyView, AnyViewState, IntoAny},
|
||||
Mountable, Position, PositionState, Render, RenderHtml,
|
||||
MarkBranch, Mountable, Position, PositionState, Render, RenderHtml,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -68,6 +69,10 @@ impl Mountable for FlatRoutesViewState {
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
|
||||
self.view.insert_before_this(child)
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
|
||||
self.view.elements()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Loc, Defs, FalFn, Fal> Render for FlatRoutesView<Loc, Defs, FalFn>
|
||||
@@ -343,7 +348,7 @@ impl<Loc, Defs, FalFn, Fal> AddAnyAttr for FlatRoutesView<Loc, Defs, FalFn>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
Defs: MatchNestedRoutes + Send + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send,
|
||||
FalFn: FnOnce() -> Fal + Send + 'static,
|
||||
Fal: RenderHtml + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: leptos::attr::Attribute> =
|
||||
@@ -360,6 +365,112 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MatchedRoute(pub String, pub AnyView);
|
||||
|
||||
impl Render for MatchedRoute {
|
||||
type State = <AnyView as Render>::State;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
self.1.build()
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.1.rebuild(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAnyAttr for MatchedRoute {
|
||||
type Output<SomeNewAttr: Attribute> = Self;
|
||||
|
||||
fn add_any_attr<NewAttr: Attribute>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
let MatchedRoute(id, view) = self;
|
||||
MatchedRoute(id, view.add_any_attr(attr).into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderHtml for MatchedRoute {
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
const MIN_LENGTH: usize = 0;
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
self.1.dry_resolve();
|
||||
}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
let MatchedRoute(id, view) = self;
|
||||
let view = view.resolve().await;
|
||||
MatchedRoute(id, view)
|
||||
}
|
||||
|
||||
fn to_html_with_buf(
|
||||
self,
|
||||
buf: &mut String,
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if mark_branches {
|
||||
buf.open_branch(&self.0);
|
||||
}
|
||||
self.1.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if mark_branches {
|
||||
buf.close_branch(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
self,
|
||||
buf: &mut StreamBuilder,
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
if mark_branches {
|
||||
buf.open_branch(&self.0);
|
||||
}
|
||||
self.1.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if mark_branches {
|
||||
buf.close_branch(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
self.1.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Loc, Defs, FalFn, Fal> FlatRoutesView<Loc, Defs, FalFn>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
@@ -392,6 +503,7 @@ where
|
||||
let view = match new_match {
|
||||
None => (self.fallback)().into_any(),
|
||||
Some(new_match) => {
|
||||
let id = new_match.as_matched().to_string();
|
||||
let (view, _) = new_match.into_view_and_child();
|
||||
let view = owner
|
||||
.with(|| {
|
||||
@@ -404,6 +516,7 @@ where
|
||||
})
|
||||
.now_or_never()
|
||||
.expect("async route used in SSR");
|
||||
let view = MatchedRoute(id, view);
|
||||
view.into_any()
|
||||
}
|
||||
};
|
||||
@@ -416,10 +529,11 @@ impl<Loc, Defs, FalFn, Fal> RenderHtml for FlatRoutesView<Loc, Defs, FalFn>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
Defs: MatchNestedRoutes + Send + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send,
|
||||
FalFn: FnOnce() -> Fal + Send + 'static,
|
||||
Fal: RenderHtml + 'static,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = <Either<Fal, AnyView> as RenderHtml>::MIN_LENGTH;
|
||||
|
||||
@@ -435,6 +549,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// if this is being run on the server for the first time, generating all possible routes
|
||||
if RouteList::is_generating() {
|
||||
@@ -481,7 +596,13 @@ where
|
||||
RouteList::register(RouteList::from(routes));
|
||||
} else {
|
||||
let view = self.choose_ssr();
|
||||
view.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
view.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +612,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -500,6 +622,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -604,4 +727,8 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
55
router/src/matching/any_choose_view.rs
Normal file
55
router/src/matching/any_choose_view.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use super::ChooseView;
|
||||
use futures::FutureExt;
|
||||
use std::{future::Future, pin::Pin};
|
||||
use tachys::{erased::Erased, view::any_view::AnyView};
|
||||
|
||||
/// A type-erased [`ChooseView`].
|
||||
pub struct AnyChooseView {
|
||||
value: Erased,
|
||||
clone: fn(&Erased) -> AnyChooseView,
|
||||
choose: fn(Erased) -> Pin<Box<dyn Future<Output = AnyView>>>,
|
||||
preload: for<'a> fn(&'a Erased) -> Pin<Box<dyn Future<Output = ()> + 'a>>,
|
||||
}
|
||||
|
||||
impl Clone for AnyChooseView {
|
||||
fn clone(&self) -> Self {
|
||||
(self.clone)(&self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyChooseView {
|
||||
pub(crate) fn new<T: ChooseView>(value: T) -> Self {
|
||||
fn clone<T: ChooseView>(value: &Erased) -> AnyChooseView {
|
||||
AnyChooseView::new(value.get_ref::<T>().clone())
|
||||
}
|
||||
|
||||
fn choose<T: ChooseView>(
|
||||
value: Erased,
|
||||
) -> Pin<Box<dyn Future<Output = AnyView>>> {
|
||||
value.into_inner::<T>().choose().boxed_local()
|
||||
}
|
||||
|
||||
fn preload<'a, T: ChooseView>(
|
||||
value: &'a Erased,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
|
||||
value.get_ref::<T>().preload().boxed_local()
|
||||
}
|
||||
|
||||
Self {
|
||||
value: Erased::new(value),
|
||||
clone: clone::<T>,
|
||||
choose: choose::<T>,
|
||||
preload: preload::<T>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChooseView for AnyChooseView {
|
||||
async fn choose(self) -> AnyView {
|
||||
(self.choose)(self.value).await
|
||||
}
|
||||
|
||||
async fn preload(&self) {
|
||||
(self.preload)(&self.value).await;
|
||||
}
|
||||
}
|
||||
@@ -130,3 +130,34 @@ tuples!(EitherOf13 => A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tuples!(EitherOf14 => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tuples!(EitherOf15 => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tuples!(EitherOf16 => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
|
||||
/// A version of [`IntoMaybeErased`] for the [`ChooseView`] trait.
|
||||
pub trait IntoChooseViewMaybeErased {
|
||||
/// The type of the erased view.
|
||||
type Output: IntoChooseViewMaybeErased;
|
||||
|
||||
/// Erase the type of the view.
|
||||
fn into_maybe_erased(self) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<T> IntoChooseViewMaybeErased for T
|
||||
where
|
||||
T: ChooseView + Send + Clone + 'static,
|
||||
{
|
||||
#[cfg(erase_components)]
|
||||
type Output = crate::matching::any_choose_view::AnyChooseView;
|
||||
|
||||
#[cfg(not(erase_components))]
|
||||
type Output = Self;
|
||||
|
||||
fn into_maybe_erased(self) -> Self::Output {
|
||||
#[cfg(erase_components)]
|
||||
{
|
||||
crate::matching::any_choose_view::AnyChooseView::new(self)
|
||||
}
|
||||
#[cfg(not(erase_components))]
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{PartialPathMatch, PathSegment};
|
||||
use std::sync::Arc;
|
||||
mod param_segments;
|
||||
mod static_segment;
|
||||
mod tuples;
|
||||
@@ -11,9 +12,37 @@ pub use static_segment::*;
|
||||
/// This is a "horizontal" matching: i.e., it treats a tuple of route segments
|
||||
/// as subsequent segments of the URL and tries to match them all.
|
||||
pub trait PossibleRouteMatch {
|
||||
const OPTIONAL: bool = false;
|
||||
fn optional(&self) -> bool;
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>);
|
||||
}
|
||||
|
||||
impl PossibleRouteMatch for Box<dyn PossibleRouteMatch + Send + Sync> {
|
||||
fn optional(&self) -> bool {
|
||||
(**self).optional()
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
(**self).test(path)
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
(**self).generate_path(path);
|
||||
}
|
||||
}
|
||||
|
||||
impl PossibleRouteMatch for Arc<dyn PossibleRouteMatch + Send + Sync> {
|
||||
fn optional(&self) -> bool {
|
||||
(**self).optional()
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
(**self).test(path)
|
||||
}
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>) {
|
||||
(**self).generate_path(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ use std::borrow::Cow;
|
||||
pub struct ParamSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for ParamSegment {
|
||||
fn optional(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let mut matched_len = 0;
|
||||
let mut param_offset = 0;
|
||||
@@ -121,6 +125,10 @@ impl PossibleRouteMatch for ParamSegment {
|
||||
pub struct WildcardSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for WildcardSegment {
|
||||
fn optional(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let mut matched_len = 0;
|
||||
let mut param_offset = 0;
|
||||
@@ -158,7 +166,9 @@ impl PossibleRouteMatch for WildcardSegment {
|
||||
pub struct OptionalParamSegment(pub &'static str);
|
||||
|
||||
impl PossibleRouteMatch for OptionalParamSegment {
|
||||
const OPTIONAL: bool = true;
|
||||
fn optional(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let mut matched_len = 0;
|
||||
|
||||
@@ -2,6 +2,10 @@ use super::{PartialPathMatch, PathSegment, PossibleRouteMatch};
|
||||
use std::fmt::Debug;
|
||||
|
||||
impl PossibleRouteMatch for () {
|
||||
fn optional(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
Some(PartialPathMatch::new(path, vec![], ""))
|
||||
}
|
||||
@@ -54,6 +58,10 @@ impl AsPath for &'static str {
|
||||
pub struct StaticSegment<T: AsPath>(pub T);
|
||||
|
||||
impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
|
||||
fn optional(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let mut matched_len = 0;
|
||||
let mut test = path.chars().peekable();
|
||||
|
||||
@@ -8,14 +8,20 @@ macro_rules! tuples {
|
||||
$first: PossibleRouteMatch,
|
||||
$($ty: PossibleRouteMatch),*,
|
||||
{
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
// on the first run, include all optionals
|
||||
let mut include_optionals = {
|
||||
[$first::OPTIONAL, $($ty::OPTIONAL),*].into_iter().filter(|n| *n).count()
|
||||
};
|
||||
|
||||
fn optional(&self) -> bool {
|
||||
#[allow(non_snake_case)]
|
||||
let ($first, $($ty,)*) = &self;
|
||||
[$first.optional(), $($ty.optional()),*].into_iter().any(|n| n)
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
#[allow(non_snake_case)]
|
||||
let ($first, $($ty,)*) = &self;
|
||||
|
||||
// on the first run, include all optionals
|
||||
let mut include_optionals = {
|
||||
[$first.optional(), $($ty.optional()),*].into_iter().filter(|n| *n).count()
|
||||
};
|
||||
|
||||
loop {
|
||||
let mut nth_field = 0;
|
||||
@@ -25,7 +31,7 @@ macro_rules! tuples {
|
||||
let mut p = Vec::new();
|
||||
let mut m = String::new();
|
||||
|
||||
if !$first::OPTIONAL || nth_field < include_optionals {
|
||||
if !$first.optional() || nth_field < include_optionals {
|
||||
match $first.test(r) {
|
||||
None => {
|
||||
return None;
|
||||
@@ -40,16 +46,16 @@ macro_rules! tuples {
|
||||
|
||||
matched_len += m.len();
|
||||
$(
|
||||
if $ty::OPTIONAL {
|
||||
if $ty.optional() {
|
||||
nth_field += 1;
|
||||
}
|
||||
if !$ty::OPTIONAL || nth_field < include_optionals {
|
||||
if !$ty.optional() || nth_field < include_optionals {
|
||||
let PartialPathMatch {
|
||||
remaining,
|
||||
matched,
|
||||
params
|
||||
} = match $ty.test(r) {
|
||||
None => if $ty::OPTIONAL {
|
||||
None => if $ty.optional() {
|
||||
return None;
|
||||
} else {
|
||||
if include_optionals == 0 {
|
||||
@@ -90,6 +96,10 @@ where
|
||||
Self: core::fmt::Debug,
|
||||
A: PossibleRouteMatch,
|
||||
{
|
||||
fn optional(&self) -> bool {
|
||||
self.0.optional()
|
||||
}
|
||||
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>> {
|
||||
let remaining = path;
|
||||
let PartialPathMatch {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
mod any_choose_view;
|
||||
mod choose_view;
|
||||
mod path_segment;
|
||||
pub(crate) mod resolve_path;
|
||||
|
||||
100
router/src/matching/nested/any_nested_match.rs
Normal file
100
router/src/matching/nested/any_nested_match.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
use crate::{
|
||||
matching::any_choose_view::AnyChooseView, ChooseView, MatchInterface,
|
||||
MatchParams, RouteMatchId,
|
||||
};
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
use tachys::erased::ErasedLocal;
|
||||
|
||||
/// A type-erased container for any [`MatchParams'] + [`MatchInterface`].
|
||||
pub struct AnyNestedMatch {
|
||||
value: ErasedLocal,
|
||||
to_params: fn(&ErasedLocal) -> Vec<(Cow<'static, str>, String)>,
|
||||
as_id: fn(&ErasedLocal) -> RouteMatchId,
|
||||
as_matched: for<'a> fn(&'a ErasedLocal) -> &'a str,
|
||||
into_view_and_child:
|
||||
fn(ErasedLocal) -> (AnyChooseView, Option<AnyNestedMatch>),
|
||||
}
|
||||
|
||||
impl Debug for AnyNestedMatch {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AnyNestedMatch").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts anything implementing [`MatchParams'] + [`MatchInterface`] into an erased type.
|
||||
pub trait IntoAnyNestedMatch {
|
||||
/// Wraps the nested route.
|
||||
fn into_any_nested_match(self) -> AnyNestedMatch;
|
||||
}
|
||||
|
||||
impl<T> IntoAnyNestedMatch for T
|
||||
where
|
||||
T: MatchParams + MatchInterface + 'static,
|
||||
{
|
||||
fn into_any_nested_match(self) -> AnyNestedMatch {
|
||||
let value = ErasedLocal::new(self);
|
||||
|
||||
fn to_params<T: MatchParams + 'static>(
|
||||
value: &ErasedLocal,
|
||||
) -> Vec<(Cow<'static, str>, String)> {
|
||||
let value = value.get_ref::<T>();
|
||||
value.to_params()
|
||||
}
|
||||
|
||||
fn as_id<T: MatchInterface + 'static>(
|
||||
value: &ErasedLocal,
|
||||
) -> RouteMatchId {
|
||||
let value = value.get_ref::<T>();
|
||||
value.as_id()
|
||||
}
|
||||
|
||||
fn as_matched<T: MatchInterface + 'static>(
|
||||
value: &ErasedLocal,
|
||||
) -> &str {
|
||||
let value = value.get_ref::<T>();
|
||||
value.as_matched()
|
||||
}
|
||||
|
||||
fn into_view_and_child<T: MatchInterface + 'static>(
|
||||
value: ErasedLocal,
|
||||
) -> (AnyChooseView, Option<AnyNestedMatch>) {
|
||||
let value = value.into_inner::<T>();
|
||||
let (view, child) = value.into_view_and_child();
|
||||
(
|
||||
AnyChooseView::new(view),
|
||||
child.map(|child| child.into_any_nested_match()),
|
||||
)
|
||||
}
|
||||
|
||||
AnyNestedMatch {
|
||||
value,
|
||||
to_params: to_params::<T>,
|
||||
as_id: as_id::<T>,
|
||||
as_matched: as_matched::<T>,
|
||||
into_view_and_child: into_view_and_child::<T>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchParams for AnyNestedMatch {
|
||||
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
|
||||
(self.to_params)(&self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchInterface for AnyNestedMatch {
|
||||
type Child = AnyNestedMatch;
|
||||
|
||||
fn as_id(&self) -> RouteMatchId {
|
||||
(self.as_id)(&self.value)
|
||||
}
|
||||
|
||||
fn as_matched(&self) -> &str {
|
||||
(self.as_matched)(&self.value)
|
||||
}
|
||||
|
||||
fn into_view_and_child(self) -> (impl ChooseView, Option<Self::Child>) {
|
||||
(self.into_view_and_child)(self.value)
|
||||
}
|
||||
}
|
||||
100
router/src/matching/nested/any_nested_route.rs
Normal file
100
router/src/matching/nested/any_nested_route.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
#![allow(clippy::type_complexity)]
|
||||
use crate::{
|
||||
matching::nested::any_nested_match::{AnyNestedMatch, IntoAnyNestedMatch},
|
||||
GeneratedRouteData, MatchNestedRoutes, RouteMatchId,
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
use tachys::{erased::Erased, prelude::IntoMaybeErased};
|
||||
|
||||
/// A type-erased container for any [`MatchNestedRoutes`].
|
||||
pub struct AnyNestedRoute {
|
||||
value: Erased,
|
||||
clone: fn(&Erased) -> AnyNestedRoute,
|
||||
match_nested:
|
||||
for<'a> fn(
|
||||
&'a Erased,
|
||||
&'a str,
|
||||
)
|
||||
-> (Option<(RouteMatchId, AnyNestedMatch)>, &'a str),
|
||||
generate_routes: fn(&Erased) -> Vec<GeneratedRouteData>,
|
||||
}
|
||||
|
||||
impl Clone for AnyNestedRoute {
|
||||
fn clone(&self) -> Self {
|
||||
(self.clone)(&self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AnyNestedRoute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AnyNestedRoute").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoMaybeErased for AnyNestedRoute {
|
||||
type Output = Self;
|
||||
|
||||
fn into_maybe_erased(self) -> Self::Output {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts anything implementing [`MatchNestedRoutes`] into [`AnyNestedRoute`].
|
||||
pub trait IntoAnyNestedRoute {
|
||||
/// Wraps the nested route.
|
||||
fn into_any_nested_route(self) -> AnyNestedRoute;
|
||||
}
|
||||
|
||||
impl<T> IntoAnyNestedRoute for T
|
||||
where
|
||||
T: MatchNestedRoutes + Send + Clone + 'static,
|
||||
{
|
||||
fn into_any_nested_route(self) -> AnyNestedRoute {
|
||||
fn clone<T: MatchNestedRoutes + Send + Clone + 'static>(
|
||||
value: &Erased,
|
||||
) -> AnyNestedRoute {
|
||||
value.get_ref::<T>().clone().into_any_nested_route()
|
||||
}
|
||||
|
||||
fn match_nested<'a, T: MatchNestedRoutes + Send + Clone + 'static>(
|
||||
value: &'a Erased,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, AnyNestedMatch)>, &'a str) {
|
||||
let (maybe_match, path) = value.get_ref::<T>().match_nested(path);
|
||||
(
|
||||
maybe_match
|
||||
.map(|(id, matched)| (id, matched.into_any_nested_match())),
|
||||
path,
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_routes<T: MatchNestedRoutes + Send + Clone + 'static>(
|
||||
value: &Erased,
|
||||
) -> Vec<GeneratedRouteData> {
|
||||
value.get_ref::<T>().generate_routes().into_iter().collect()
|
||||
}
|
||||
|
||||
AnyNestedRoute {
|
||||
value: Erased::new(self),
|
||||
clone: clone::<T>,
|
||||
match_nested: match_nested::<T>,
|
||||
generate_routes: generate_routes::<T>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchNestedRoutes for AnyNestedRoute {
|
||||
type Data = AnyNestedMatch;
|
||||
type Match = AnyNestedMatch;
|
||||
|
||||
fn match_nested<'a>(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
|
||||
(self.match_nested)(&self.value, path)
|
||||
}
|
||||
|
||||
fn generate_routes(&self) -> impl IntoIterator<Item = GeneratedRouteData> {
|
||||
(self.generate_routes)(&self.value)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
MatchInterface, MatchNestedRoutes, PartialPathMatch, PathSegment,
|
||||
PossibleRouteMatch, RouteMatchId,
|
||||
IntoChooseViewMaybeErased, MatchInterface, MatchNestedRoutes,
|
||||
PartialPathMatch, PathSegment, PossibleRouteMatch, RouteMatchId,
|
||||
};
|
||||
use crate::{ChooseView, GeneratedRouteData, MatchParams, Method, SsrMode};
|
||||
use core::{fmt, iter};
|
||||
@@ -10,7 +10,10 @@ use std::{
|
||||
collections::HashSet,
|
||||
sync::atomic::{AtomicU16, Ordering},
|
||||
};
|
||||
use tachys::prelude::IntoMaybeErased;
|
||||
|
||||
pub mod any_nested_match;
|
||||
pub mod any_nested_route;
|
||||
mod tuples;
|
||||
|
||||
pub(crate) static ROUTE_ID: AtomicU16 = AtomicU16::new(1);
|
||||
@@ -26,6 +29,31 @@ pub struct NestedRoute<Segments, Children, Data, View> {
|
||||
ssr_mode: SsrMode,
|
||||
}
|
||||
|
||||
impl<Segments, Children, Data, View> IntoMaybeErased
|
||||
for NestedRoute<Segments, Children, Data, View>
|
||||
where
|
||||
Self: MatchNestedRoutes + Send + Clone + 'static,
|
||||
{
|
||||
#[cfg(erase_components)]
|
||||
type Output = any_nested_route::AnyNestedRoute;
|
||||
|
||||
#[cfg(not(erase_components))]
|
||||
type Output = Self;
|
||||
|
||||
fn into_maybe_erased(self) -> Self::Output {
|
||||
#[cfg(erase_components)]
|
||||
{
|
||||
use any_nested_route::IntoAnyNestedRoute;
|
||||
|
||||
self.into_any_nested_route()
|
||||
}
|
||||
#[cfg(not(erase_components))]
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Segments, Children, Data, View> Clone
|
||||
for NestedRoute<Segments, Children, Data, View>
|
||||
where
|
||||
@@ -48,16 +76,24 @@ where
|
||||
}
|
||||
|
||||
impl<Segments, View> NestedRoute<Segments, (), (), View> {
|
||||
pub fn new(path: Segments, view: View) -> Self
|
||||
pub fn new(
|
||||
path: Segments,
|
||||
view: View,
|
||||
) -> NestedRoute<
|
||||
Segments,
|
||||
(),
|
||||
(),
|
||||
<View as IntoChooseViewMaybeErased>::Output,
|
||||
>
|
||||
where
|
||||
View: ChooseView,
|
||||
{
|
||||
Self {
|
||||
NestedRoute {
|
||||
id: ROUTE_ID.fetch_add(1, Ordering::Relaxed),
|
||||
segments: path,
|
||||
children: None,
|
||||
data: (),
|
||||
view,
|
||||
view: view.into_maybe_erased(),
|
||||
methods: [Method::Get].into(),
|
||||
ssr_mode: Default::default(),
|
||||
}
|
||||
@@ -151,7 +187,7 @@ impl<Segments, Children, Data, View> MatchNestedRoutes
|
||||
for NestedRoute<Segments, Children, Data, View>
|
||||
where
|
||||
Self: 'static,
|
||||
Segments: PossibleRouteMatch + std::fmt::Debug,
|
||||
Segments: PossibleRouteMatch,
|
||||
Children: MatchNestedRoutes,
|
||||
Children::Match: MatchParams,
|
||||
Children: 'static,
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{ChooseView, GeneratedRouteData, MatchParams};
|
||||
use core::iter;
|
||||
use either_of::*;
|
||||
use std::borrow::Cow;
|
||||
use tachys::view::iterators::StaticVec;
|
||||
|
||||
impl MatchParams for () {
|
||||
fn to_params(&self) -> Vec<(Cow<'static, str>, String)> {
|
||||
@@ -181,6 +182,32 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MatchNestedRoutes for StaticVec<T>
|
||||
where
|
||||
T: MatchNestedRoutes,
|
||||
{
|
||||
type Data = Vec<T::Data>;
|
||||
type Match = T::Match;
|
||||
|
||||
fn match_nested<'a>(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
) -> (Option<(RouteMatchId, Self::Match)>, &'a str) {
|
||||
for item in self.iter() {
|
||||
if let (Some((id, matched)), remaining) = item.match_nested(path) {
|
||||
return (Some((id, matched)), remaining);
|
||||
}
|
||||
}
|
||||
(None, path)
|
||||
}
|
||||
|
||||
fn generate_routes(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = GeneratedRouteData> + '_ {
|
||||
self.iter().flat_map(T::generate_routes)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! chain_generated {
|
||||
($first:expr, $second:expr, ) => {
|
||||
$first.chain($second)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
flat_router::MatchedRoute,
|
||||
hooks::Matched,
|
||||
location::{LocationProvider, Url},
|
||||
matching::RouteDefs,
|
||||
@@ -10,7 +11,7 @@ use crate::{
|
||||
use any_spawner::Executor;
|
||||
use either_of::{Either, EitherOf3};
|
||||
use futures::{channel::oneshot, future::join_all, FutureExt};
|
||||
use leptos::{component, oco::Oco};
|
||||
use leptos::{attr::any_attribute::AnyAttribute, component, oco::Oco};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use reactive_graph::{
|
||||
computed::{ArcMemo, ScopedFuture},
|
||||
@@ -228,8 +229,8 @@ where
|
||||
impl<Loc, Defs, Fal, FalFn> AddAnyAttr for NestedRoutesView<Loc, Defs, FalFn>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
Defs: MatchNestedRoutes + Send,
|
||||
FalFn: FnOnce() -> Fal + Send,
|
||||
Defs: MatchNestedRoutes + Send + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send + 'static,
|
||||
Fal: RenderHtml + 'static,
|
||||
{
|
||||
type Output<SomeNewAttr: leptos::attr::Attribute> =
|
||||
@@ -249,11 +250,12 @@ where
|
||||
impl<Loc, Defs, FalFn, Fal> RenderHtml for NestedRoutesView<Loc, Defs, FalFn>
|
||||
where
|
||||
Loc: LocationProvider + Send,
|
||||
Defs: MatchNestedRoutes + Send,
|
||||
FalFn: FnOnce() -> Fal + Send,
|
||||
Defs: MatchNestedRoutes + Send + 'static,
|
||||
FalFn: FnOnce() -> Fal + Send + 'static,
|
||||
Fal: RenderHtml + 'static,
|
||||
{
|
||||
type AsyncOutput = Self;
|
||||
type Owned = Self;
|
||||
|
||||
const MIN_LENGTH: usize = 0; // TODO
|
||||
|
||||
@@ -269,6 +271,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
// if this is being run on the server for the first time, generating all possible routes
|
||||
if RouteList::is_generating() {
|
||||
@@ -348,7 +351,13 @@ where
|
||||
outer_owner.with(|| Either::Right(Outlet().into_any()))
|
||||
}
|
||||
};
|
||||
view.to_html_with_buf(buf, position, escape, mark_branches);
|
||||
view.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +367,7 @@ where
|
||||
position: &mut Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -400,6 +410,7 @@ where
|
||||
position,
|
||||
escape,
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -456,6 +467,10 @@ where
|
||||
view,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
type OutletViewFn = Box<dyn FnMut() -> Suspend<AnyView> + Send>;
|
||||
@@ -628,21 +643,28 @@ where
|
||||
async move {
|
||||
provide_context(params_including_parents);
|
||||
provide_context(url);
|
||||
provide_context(matched);
|
||||
provide_context(matched.clone());
|
||||
view.preload().await;
|
||||
*view_fn.lock().or_poisoned() = Box::new(move || {
|
||||
let view = view.clone();
|
||||
owner.with(|| {
|
||||
Suspend::new(Box::pin(async move {
|
||||
let view = SendWrapper::new(ScopedFuture::new(
|
||||
view.choose(),
|
||||
));
|
||||
let view = view.await;
|
||||
OwnedView::new(view).into_any()
|
||||
})
|
||||
as Pin<
|
||||
Box<dyn Future<Output = AnyView> + Send>,
|
||||
>)
|
||||
owner.with({
|
||||
let matched = matched.clone();
|
||||
move || {
|
||||
Suspend::new(Box::pin(async move {
|
||||
let view = SendWrapper::new(
|
||||
ScopedFuture::new(view.choose()),
|
||||
);
|
||||
let view = view.await;
|
||||
let view =
|
||||
MatchedRoute(matched.0.get(), view);
|
||||
OwnedView::new(view).into_any()
|
||||
})
|
||||
as Pin<
|
||||
Box<
|
||||
dyn Future<Output = AnyView> + Send,
|
||||
>,
|
||||
>)
|
||||
}
|
||||
})
|
||||
});
|
||||
trigger
|
||||
@@ -877,6 +899,10 @@ where
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
|
||||
self.view.insert_before_this(child)
|
||||
}
|
||||
|
||||
fn elements(&self) -> Vec<tachys::renderer::types::Element> {
|
||||
self.view.elements()
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays the child route nested in a parent route, allowing you to control exactly where
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user