mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 09:02:37 -05:00
Compare commits
81 Commits
hash-file-
...
4063
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceed5e40e4 | ||
|
|
f4e0be2d59 | ||
|
|
05f50f7d27 | ||
|
|
a22d6f58be | ||
|
|
ff21c9cae2 | ||
|
|
733a353820 | ||
|
|
829b07b598 | ||
|
|
0df6cd74ee | ||
|
|
1da833a0aa | ||
|
|
f37d124d6a | ||
|
|
5d0e683b0f | ||
|
|
f34e3a5bc9 | ||
|
|
d7dd6a1109 | ||
|
|
ff81d34084 | ||
|
|
40a7aba3bc | ||
|
|
d4dcafd908 | ||
|
|
82ccbbf806 | ||
|
|
5ba45bb1ed | ||
|
|
06dfa37eee | ||
|
|
e82a0bbc7f | ||
|
|
4a972fc09e | ||
|
|
07cf649e3b | ||
|
|
0e9598b799 | ||
|
|
82303d7e33 | ||
|
|
c4354ac965 | ||
|
|
7de550685a | ||
|
|
b1f3f6023e | ||
|
|
c189c3a45d | ||
|
|
3903867f82 | ||
|
|
a42fa452fc | ||
|
|
cd48a6ac8c | ||
|
|
34c14adcb8 | ||
|
|
50cee1d614 | ||
|
|
7ca691305f | ||
|
|
830882f330 | ||
|
|
13110a35e2 | ||
|
|
304dc081a2 | ||
|
|
14f6bc658e | ||
|
|
09894aaca9 | ||
|
|
2ee4444bb4 | ||
|
|
03a1c1e7a6 | ||
|
|
12e49ed996 | ||
|
|
1e281e9e74 | ||
|
|
bd475f89d0 | ||
|
|
3d91b5e90f | ||
|
|
96d8d5218c | ||
|
|
84caa35cef | ||
|
|
fc8b55161c | ||
|
|
657052466b | ||
|
|
efe8336363 | ||
|
|
770881842c | ||
|
|
0d540ef02f | ||
|
|
dc1885ad92 | ||
|
|
61bf87439a | ||
|
|
308568e520 | ||
|
|
1b0f32dc4c | ||
|
|
2e0b3011d9 | ||
|
|
680d4ccd07 | ||
|
|
325f9cbe33 | ||
|
|
26ab392c95 | ||
|
|
3a4e2a19aa | ||
|
|
eed3d21b40 | ||
|
|
7ae386285d | ||
|
|
ebcc51136d | ||
|
|
e10ded4fd0 | ||
|
|
67be872f58 | ||
|
|
e5b21ac0fc | ||
|
|
9b2e313d20 | ||
|
|
bc79232033 | ||
|
|
a7bb2565c4 | ||
|
|
2e393aaca0 | ||
|
|
e37711cb85 | ||
|
|
627b553e60 | ||
|
|
e2f9aca466 | ||
|
|
970544ed0b | ||
|
|
0c3b3c440f | ||
|
|
6578086e09 | ||
|
|
113aba9666 | ||
|
|
58d7475193 | ||
|
|
04d80ff8d0 | ||
|
|
7971a2dccb |
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -33,10 +33,11 @@ Steps to reproduce the behavior:
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Next Steps**
|
||||
[ ] I will make a PR
|
||||
[ ] I would like to make a PR, but need help getting started
|
||||
[ ] I want someone else to take the time to fix this
|
||||
[ ] This is a low priority for me and is just shared for your information
|
||||
|
||||
- [ ] I will make a PR
|
||||
- [ ] I would like to make a PR, but need help getting started
|
||||
- [ ] I want someone else to take the time to fix this
|
||||
- [ ] This is a low priority for me and is just shared for your information
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
28
.github/workflows/autofix.yml
vendored
28
.github/workflows/autofix.yml
vendored
@@ -21,33 +21,19 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
with: {toolchain: "nightly-2025-04-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
|
||||
- name: Install Glib
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libglib2.0-dev
|
||||
- name: Install cargo-all-features
|
||||
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
|
||||
- name: Install jq
|
||||
run: sudo apt-get install jq
|
||||
- run: |
|
||||
echo "Formatting the workspace"
|
||||
cargo fmt --all
|
||||
|
||||
echo "Running Clippy against each member's features (default features included)"
|
||||
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
|
||||
echo "Working on member $member":
|
||||
echo -e "\tdefault-features/no-features:"
|
||||
# this will also run on members with no features or default features
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member"
|
||||
|
||||
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
|
||||
for feature in $features; do
|
||||
if [ "$feature" = "default" ]; then
|
||||
continue
|
||||
fi
|
||||
echo -e "\tfeature $feature"
|
||||
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
|
||||
done
|
||||
done
|
||||
- name: Format the workspace
|
||||
run: cargo fmt --all
|
||||
- name: Clippy the workspace
|
||||
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
|
||||
- uses: autofix-ci/action@v1.3.1
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
8
.github/workflows/run-cargo-make-task.yml
vendored
8
.github/workflows/run-cargo-make-task.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain: [stable, nightly-2025-03-05]
|
||||
toolchain: [stable, nightly-2025-04-16]
|
||||
erased_mode: [true, false]
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
@@ -160,6 +160,12 @@ jobs:
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
cargo make --no-workspace --profile=github-actions ci
|
||||
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
|
||||
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
|
||||
if: contains(inputs.directory, 'counter_isomorphic')
|
||||
run: |
|
||||
cd '${{ inputs.directory }}'
|
||||
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
|
||||
- name: Clean up ${{ inputs.directory }}
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
728
Cargo.lock
generated
728
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
91
Cargo.toml
91
Cargo.toml
@@ -40,44 +40,81 @@ members = [
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.0-rc3"
|
||||
version = "0.8.2"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
convert_case = "0.8"
|
||||
# members
|
||||
throw_error = { path = "./any_error/", version = "0.3.0" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc3" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.3.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.3.0" }
|
||||
itertools = "0.14.0"
|
||||
leptos = { path = "./leptos", version = "0.8.0-rc3" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.0-rc3" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc3" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc3" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc3" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc3" }
|
||||
leptos_router = { path = "./router", version = "0.8.0-rc3" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc3" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.0-rc3" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.0-rc3" }
|
||||
leptos = { path = "./leptos", version = "0.8.2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.8.2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.8.2" }
|
||||
leptos_router = { path = "./router", version = "0.8.2" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.8.2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.8.2" }
|
||||
leptos_meta = { path = "./meta", version = "0.8.2" }
|
||||
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.2.0-rc3" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc3" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc3" }
|
||||
rustversion = "1"
|
||||
serde_json = "1.0.0"
|
||||
server_fn = { path = "./server_fn", version = "0.8.0-rc3" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc3" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc3" }
|
||||
tachys = { path = "./tachys", version = "0.2.0-rc3" }
|
||||
trybuild = "1"
|
||||
typed-builder = "0.21.0"
|
||||
thiserror = "2.0.12"
|
||||
wasm-bindgen = "0.2.100"
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.2.0" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.2.0" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.8.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.8.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.2" }
|
||||
tachys = { path = "./tachys", version = "0.2.0" }
|
||||
|
||||
# common deps
|
||||
itertools = { default-features = false, version = "0.14.0" }
|
||||
convert_case = { default-features = false, version = "0.8.0" }
|
||||
serde_json = { default-features = false, version = "1.0" }
|
||||
trybuild = { default-features = false, version = "1.0" }
|
||||
typed-builder = { default-features = false, version = "0.21.0" }
|
||||
thiserror = { default-features = false, version = "2.0" }
|
||||
wasm-bindgen = { default-features = false, version = "0.2.100" }
|
||||
indexmap = { default-features = false, version = "2.9" }
|
||||
rstml = { default-features = false, version = "0.12.1" }
|
||||
rustc_version = { default-features = false, version = "0.4.1" }
|
||||
guardian = { default-features = false, version = "1.3" }
|
||||
rustc-hash = { default-features = false, version = "2.1" }
|
||||
once_cell = { default-features = false, version = "1.21" }
|
||||
actix-web = { default-features = false, version = "4.10" }
|
||||
tracing = { default-features = false, version = "0.1.41" }
|
||||
slotmap = { default-features = false, version = "1.0" }
|
||||
futures = { default-features = false, version = "0.3.31" }
|
||||
dashmap = { default-features = false, version = "6.1" }
|
||||
pin-project-lite = { default-features = false, version = "0.2.16" }
|
||||
send_wrapper = { default-features = false, version = "0.6.0" }
|
||||
tokio-test = { default-features = false, version = "0.4.4" }
|
||||
html-escape = { default-features = false, version = "0.2.13" }
|
||||
proc-macro-error2 = { default-features = false, version = "2.0" }
|
||||
const_format = { default-features = false, version = "0.2.34" }
|
||||
gloo-net = { default-features = false, version = "0.6.0" }
|
||||
url = { default-features = false, version = "2.5" }
|
||||
tokio = { default-features = false, version = "1.44" }
|
||||
base64 = { default-features = false, version = "0.22.1" }
|
||||
cfg-if = { default-features = false, version = "1.0" }
|
||||
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
|
||||
tower = { default-features = false, version = "0.5.2" }
|
||||
proc-macro2 = { default-features = false, version = "1.0" }
|
||||
serde = { default-features = false, version = "1.0" }
|
||||
parking_lot = { default-features = false, version = "0.12.3" }
|
||||
axum = { default-features = false, version = "0.8.3" }
|
||||
serde_qs = { default-features = false, version = "0.15.0" }
|
||||
syn = { default-features = false, version = "2.0" }
|
||||
xxhash-rust = { default-features = false, version = "0.8.15" }
|
||||
paste = { default-features = false, version = "1.0" }
|
||||
quote = { default-features = false, version = "1.0" }
|
||||
web-sys = { default-features = false, version = "0.3.77" }
|
||||
js-sys = { default-features = false, version = "0.3.77" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -10,4 +10,4 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.15"
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "any_spawner"
|
||||
version = "0.3.0-rc3"
|
||||
version = "0.3.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -10,22 +10,22 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-executor = { version = "1.13.1", optional = true }
|
||||
futures = "0.3.31"
|
||||
glib = { version = "0.20.6", optional = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1.41", optional = true, default-features = false, features = [
|
||||
futures = { workspace = true, default-features = true }
|
||||
glib = { version = "0.20.9", optional = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
tokio = { optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
] , workspace = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen-futures = { optional = true , workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures-lite = { version = "2.6.0", default-features = false }
|
||||
tokio = { version = "1.41", default-features = false, features = [
|
||||
tokio = { default-features = false, features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
] }
|
||||
] , workspace = true }
|
||||
wasm-bindgen-test = { version = "0.3.50" }
|
||||
serial_test = "3.2.0"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ pub const fn const_concat(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
@@ -59,7 +59,7 @@ pub const fn const_concat_with_prefix(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
@@ -116,7 +116,7 @@ pub const fn const_concat_with_separator(
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
// no mutable references in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
|
||||
@@ -10,8 +10,8 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.16"
|
||||
paste = "1.0.15"
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["no_std"]
|
||||
|
||||
@@ -7,7 +7,7 @@ pub fn main() {
|
||||
|
||||
fmt()
|
||||
.with_writer(
|
||||
// To avoide trace events in the browser from showing their
|
||||
// To avoid trace events in the browser from showing their
|
||||
// JS backtrace, which is very annoying, in my opinion
|
||||
MakeConsoleWriter::default()
|
||||
.map_trace_level_to(tracing::Level::DEBUG),
|
||||
|
||||
@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -26,12 +26,17 @@ pub async fn file_and_error_handler(
|
||||
.map(|h| h.to_str().unwrap_or("none"))
|
||||
.unwrap_or("none")
|
||||
.to_string();
|
||||
let res = get_static_file(uri.clone(), accept_encoding).await.unwrap();
|
||||
let static_result = get_static_file(uri.clone(), accept_encoding).await;
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "Not found.").into_response()
|
||||
match static_result {
|
||||
Ok(res) => {
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "Not found.").into_response()
|
||||
}
|
||||
}
|
||||
Err(e) => e.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn Stories() -> impl IntoView {
|
||||
let stories = Resource::new(
|
||||
move || (page(), story_type()),
|
||||
move |(page, story_type)| async move {
|
||||
fetch_stories(category(&story_type), page).await.ok()
|
||||
fetch_stories(story_type, page).await.ok()
|
||||
},
|
||||
);
|
||||
let (pending, set_pending) = signal(false);
|
||||
|
||||
@@ -13,7 +13,7 @@ pub async fn fetch_story(
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
|
||||
@@ -7,7 +7,7 @@ use send_wrapper::SendWrapper;
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = Resource::new(
|
||||
let story = Resource::new_blocking(
|
||||
move || params.read().get("id").unwrap_or_default(),
|
||||
move |id| {
|
||||
SendWrapper::new(async move {
|
||||
|
||||
@@ -10,7 +10,11 @@ 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",
|
||||
"islands-router",
|
||||
] }
|
||||
leptos_router = { path = "../../router" }
|
||||
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
|
||||
leptos_axum = { path = "../../integrations/axum", features = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Leptos Counter Example
|
||||
# Stores Example
|
||||
|
||||
This example creates a simple counter in a client side rendered app with Rust and WASM!
|
||||
This example shows how to use reactive stores, by building a client-side rendered TODO application.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -561,7 +561,7 @@ fn ShowCounters() -> impl IntoView {
|
||||
//
|
||||
// However, upon `Reset Counters`, the mode from which the reset
|
||||
// was issued will result in the rendering be reflected as such, so
|
||||
// if the intial state was SSR, resetting under CSR will result in
|
||||
// if the initial state was SSR, resetting under CSR will result in
|
||||
// the CSR counters be rendered after. However for the intents and
|
||||
// purpose for the testing only the CSR is cared for.
|
||||
//
|
||||
|
||||
10
examples/websocket/e2e/tests/fixtures/action.rs
vendored
10
examples/websocket/e2e/tests/fixtures/action.rs
vendored
@@ -10,19 +10,9 @@ pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_text(client: &Client, text: &str) -> Result<String> {
|
||||
fill_input(client, text).await?;
|
||||
get_label(client).await
|
||||
}
|
||||
|
||||
pub async fn fill_input(client: &Client, text: &str) -> Result<()> {
|
||||
let textbox = find::input(client).await;
|
||||
textbox.send_keys(text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_label(client: &Client) -> Result<String> {
|
||||
let label = find::label(client).await;
|
||||
Ok(label.text().await?)
|
||||
}
|
||||
|
||||
10
examples/websocket/e2e/tests/fixtures/find.rs
vendored
10
examples/websocket/e2e/tests/fixtures/find.rs
vendored
@@ -9,13 +9,3 @@ pub async fn input(client: &Client) -> Element {
|
||||
|
||||
textbox
|
||||
}
|
||||
|
||||
pub async fn label(client: &Client) -> Element {
|
||||
let label = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("p"))
|
||||
.await
|
||||
.expect("");
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
#[given(regex = "^I add a text as (.*)$")]
|
||||
async fn i_add_a_text(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::add_text(client, text.as_str()).await?;
|
||||
action::fill_input(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ async fn i_see_the_label_of_the_input_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "p", &text).await?;
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
throw_error = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
once_cell = "1.20"
|
||||
pin-project-lite = "0.2.15"
|
||||
futures = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"] , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, optional = true , default-features = true }
|
||||
js-sys = { optional = true , workspace = true, default-features = true }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
browser = ["dep:wasm-bindgen", "dep:js-sys"]
|
||||
|
||||
@@ -9,10 +9,10 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.9"
|
||||
actix-http = "3.10"
|
||||
actix-files = "0.6"
|
||||
actix-web = "4.9"
|
||||
futures = "0.3.31"
|
||||
actix-web = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
@@ -22,13 +22,13 @@ 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 }
|
||||
tokio = { version = "1.43", features = ["rt", "fs"] }
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = "6"
|
||||
once_cell = "1"
|
||||
serde_json = { workspace = true , default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
tokio = { features = ["rt", "fs"] , workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
@@ -4,18 +4,18 @@ authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
version = "0.8.0-rc3"
|
||||
version = "0.8.2"
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
hydration_context = { workspace = true }
|
||||
axum = { version = "0.8.1", default-features = false, features = [
|
||||
axum = { default-features = false, features = [
|
||||
"matched-path",
|
||||
] }
|
||||
dashmap = "6"
|
||||
futures = "0.3.31"
|
||||
] , workspace = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
leptos = { workspace = true, features = ["nonce", "ssr"] }
|
||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||
leptos_macro = { workspace = true, features = ["axum"] }
|
||||
@@ -23,16 +23,16 @@ 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 }
|
||||
tower = { version = "0.5.1", features = ["util"] }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
tokio = { default-features = false , workspace = true }
|
||||
tower = { features = ["util"] , workspace = true, default-features = true }
|
||||
tower-http = "0.6.2"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.8.1"
|
||||
tokio = { version = "1.43", features = ["net", "rt-multi-thread"] }
|
||||
axum = { workspace = true, default-features = true }
|
||||
tokio = { features = ["net", "rt-multi-thread"] , workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
|
||||
@@ -590,7 +590,7 @@ where
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -796,7 +796,7 @@ where
|
||||
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
|
||||
/// sending down its HTML. The app will become interactive once it has fully loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -879,7 +879,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_response_inner<IV>(
|
||||
/// Can be used in conjunction with a custom [file_and_error_handler_with_context] to process an Axum [Request](axum::extract::Request) into an Axum [Response](axum::response::Response)
|
||||
pub fn handle_response_inner<IV>(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
app_fn: impl FnOnce() -> IV + Send + 'static,
|
||||
req: Request<Body>,
|
||||
@@ -1022,7 +1023,7 @@ where
|
||||
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
@@ -1089,7 +1090,7 @@ where
|
||||
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
|
||||
/// `async` resources have loaded.
|
||||
///
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```
|
||||
|
||||
@@ -9,7 +9,7 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.31"
|
||||
futures = { workspace = true, default-features = true }
|
||||
hydration_context = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nonce"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
|
||||
@@ -15,8 +15,8 @@ any_spawner = { workspace = true, features = [
|
||||
"wasm-bindgen",
|
||||
"futures-executor",
|
||||
] }
|
||||
base64 = { version = "0.22.1", optional = true }
|
||||
cfg-if = "1.0"
|
||||
base64 = { optional = true, workspace = true, default-features = true }
|
||||
cfg-if = { workspace = true, default-features = true }
|
||||
hydration_context = { workspace = true }
|
||||
either_of = { workspace = true }
|
||||
leptos_dom = { workspace = true }
|
||||
@@ -27,35 +27,35 @@ leptos_config = { workspace = true }
|
||||
leptos-spin-macro = { version = "0.2.0", optional = true }
|
||||
oco_ref = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
paste = "1.0"
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
# NOTE: While not used directly, `getrandom`'s `js` feature is needed when `rand` is used on WASM to
|
||||
paste = { workspace = true, default-features = true }
|
||||
rand = { version = "0.9.1", optional = true }
|
||||
# NOTE: While not used directly, `getrandom`'s `wasm_js` feature is needed when `rand` is used on WASM to
|
||||
# avoid a compilation error
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
getrandom = { version = "0.3.3", optional = true }
|
||||
reactive_graph = { workspace = true, features = ["serde"] }
|
||||
rustc-hash = "2.0"
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
tachys = { workspace = true, features = [
|
||||
"reactive_graph",
|
||||
"reactive_stores",
|
||||
"oco",
|
||||
] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
typed-builder = { workspace = true }
|
||||
thiserror = { workspace = true, default-features = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
typed-builder = { workspace = true, default-features = true }
|
||||
typed-builder-macro = "0.21.0"
|
||||
serde = "1.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { workspace = true, default-features = true }
|
||||
serde_json = { optional = true, workspace = true, default-features = true }
|
||||
server_fn = { workspace = true, features = ["form-redirects", "browser"] }
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
web-sys = { features = [
|
||||
"ShadowRoot",
|
||||
"ShadowRootInit",
|
||||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_qs = "0.14.0"
|
||||
slotmap = "1.0"
|
||||
futures = "0.3.31"
|
||||
send_wrapper = "0.6.0"
|
||||
], workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, default-features = true }
|
||||
serde_qs = { workspace = true, default-features = true }
|
||||
slotmap = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
hydration = [
|
||||
@@ -64,13 +64,13 @@ hydration = [
|
||||
"hydration_context/browser",
|
||||
"leptos_dom/hydration",
|
||||
]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/js"]
|
||||
csr = ["leptos_macro/csr", "reactive_graph/effects", "getrandom?/wasm_js"]
|
||||
hydrate = [
|
||||
"leptos_macro/hydrate",
|
||||
"hydration",
|
||||
"tachys/hydrate",
|
||||
"reactive_graph/effects",
|
||||
"getrandom?/js",
|
||||
"getrandom?/wasm_js",
|
||||
]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
rustls = ["server_fn/rustls"]
|
||||
@@ -99,9 +99,10 @@ trace-component-props = [
|
||||
"leptos_dom/trace-component-props",
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
islands-router = ["tachys/mark_branches"]
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
use rustc_version::{version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
let target = std::env::var("TARGET").unwrap_or_default();
|
||||
|
||||
// Set cfg flags depending on release channel
|
||||
if matches!(version_meta().unwrap().channel, Channel::Nightly) {
|
||||
println!("cargo:rustc-cfg=rustc_nightly");
|
||||
}
|
||||
// Set cfg flag for getrandom wasm_js
|
||||
if target == "wasm32-unknown-unknown" {
|
||||
// Set a custom cfg flag for wasm builds
|
||||
println!("cargo:rustc-cfg=getrandom_backend=\"wasm_js\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView + Send>;
|
||||
///
|
||||
/// Different component types take different types for their `children` prop, some of which cannot
|
||||
/// be directly constructed. Using `ToChildren` allows the component user to pass children without
|
||||
/// explicity constructing the correct type.
|
||||
/// explicitly constructing the correct type.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
|
||||
@@ -255,7 +255,7 @@ where
|
||||
) -> Result<Self, serde_qs::Error>;
|
||||
}
|
||||
|
||||
/// Errors that can arise when coverting from an HTML event or form into a Rust data type.
|
||||
/// Errors that can arise when converting from an HTML event or form into a Rust data type.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FromFormDataError {
|
||||
/// Could not find a `<form>` connected to the event.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
((root, pkg_path, output_name, wasm_output_name) => {
|
||||
let MOST_RECENT_CHILDREN_CB;
|
||||
let MOST_RECENT_CHILDREN_CB = [];
|
||||
|
||||
function idle(c) {
|
||||
if ("requestIdleCallback" in window) {
|
||||
@@ -22,12 +22,18 @@
|
||||
traverse(child, children);
|
||||
}
|
||||
} else {
|
||||
if(tag === 'leptos-children') {
|
||||
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
|
||||
if (tag === 'leptos-children') {
|
||||
MOST_RECENT_CHILDREN_CB.push(node.$$on_hydrate);
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
// un-set the "most recent children"
|
||||
MOST_RECENT_CHILDREN_CB.pop();
|
||||
} else {
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
}
|
||||
for(const child of node.children) {
|
||||
traverse(child);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +43,9 @@
|
||||
function hydrateIsland(el, id, mod) {
|
||||
const islandFn = mod[id];
|
||||
if (islandFn) {
|
||||
if (MOST_RECENT_CHILDREN_CB) {
|
||||
MOST_RECENT_CHILDREN_CB();
|
||||
const children_cb = MOST_RECENT_CHILDREN_CB[MOST_RECENT_CHILDREN_CB.length-1];
|
||||
if (children_cb) {
|
||||
children_cb();
|
||||
}
|
||||
islandFn(el);
|
||||
} else {
|
||||
@@ -48,7 +55,7 @@
|
||||
idle(() => {
|
||||
import(`${root}/${pkg_path}/${output_name}.js`)
|
||||
.then(mod => {
|
||||
mod.default(`${root}/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
|
||||
mod.default({module_or_path: `${root}/${pkg_path}/${wasm_output_name}.wasm`}).then(() => {
|
||||
mod.hydrate();
|
||||
hydrateIslands(document.body, mod);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,10 @@ window.addEventListener("popstate", async (ev) => {
|
||||
});
|
||||
|
||||
window.addEventListener("submit", async (ev) => {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
const req = submitToReq(ev);
|
||||
if(!req) {
|
||||
return;
|
||||
@@ -194,6 +198,15 @@ function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
|
||||
else if (oldNode.nodeType === Node.TEXT_NODE) {
|
||||
oldNode.textContent = newNode.textContent;
|
||||
}
|
||||
// islands should not be diffed on the client, because we do not want to overwrite client-side state
|
||||
// but their children should be diffed still, because they could contain new server content
|
||||
else if (oldNode.nodeType === Node.ELEMENT_NODE && oldNode.tagName === "LEPTOS-ISLAND") {
|
||||
// TODO: diff the leptos-children
|
||||
|
||||
// skip over leptos-island otherwise
|
||||
oldDocWalker.nextSibling();
|
||||
newDocWalker.nextSibling();
|
||||
}
|
||||
// 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);
|
||||
|
||||
@@ -83,6 +83,10 @@ pub fn HydrationScripts(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
leptos::logging::error!(
|
||||
"File hashing is active but no hash file was found"
|
||||
);
|
||||
}
|
||||
} else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
|
||||
wasm_file_name.push_str("_bg");
|
||||
@@ -107,21 +111,19 @@ pub fn HydrationScripts(
|
||||
.unwrap_or_default();
|
||||
|
||||
let root = root.unwrap_or_default();
|
||||
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>
|
||||
}
|
||||
})
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ ws.onmessage = (ev) => {
|
||||
let found = false;
|
||||
document.querySelectorAll("link").forEach((link) => {
|
||||
if (link.getAttribute('href').includes(msg.css)) {
|
||||
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
|
||||
let newHref = '/' + msg.css + '?version=' + Date.now();
|
||||
link.setAttribute('href', newHref);
|
||||
found = true;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use base64::{
|
||||
engine::{self, general_purpose},
|
||||
Engine,
|
||||
};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use rand::{rng, RngCore};
|
||||
use std::{fmt::Display, ops::Deref, sync::Arc};
|
||||
use tachys::html::attribute::AttributeValue;
|
||||
|
||||
@@ -171,9 +171,9 @@ const NONCE_ENGINE: engine::GeneralPurpose =
|
||||
impl Nonce {
|
||||
/// Generates a new nonce from 16 bytes (128 bits) of random data.
|
||||
pub fn new() -> Self {
|
||||
let mut thread_rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
let mut bytes = [0; 16];
|
||||
thread_rng.fill_bytes(&mut bytes);
|
||||
rng.fill_bytes(&mut bytes);
|
||||
Nonce(NONCE_ENGINE.encode(bytes).into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
///
|
||||
/// Useful for inserting modals and tooltips outside of a cropping layout.
|
||||
/// If no mount point is given, the portal is inserted in `document.body`;
|
||||
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrappend in a `<g>`.
|
||||
/// it is wrapped in a `<div>` unless `is_svg` is `true` in which case it's wrapped in a `<g>`.
|
||||
/// Setting `use_shadow` to `true` places the element in a shadow root to isolate styles.
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
|
||||
#[component]
|
||||
@@ -42,11 +42,15 @@ where
|
||||
let children = children.into_inner();
|
||||
|
||||
Effect::new(move |_| {
|
||||
let tag = if is_svg { "g" } else { "div" };
|
||||
|
||||
let container = document()
|
||||
.create_element(tag)
|
||||
.expect("element creation to work");
|
||||
let container = if is_svg {
|
||||
document()
|
||||
.create_element_ns(Some("http://www.w3.org/2000/svg"), "g")
|
||||
.expect("SVG element creation to work")
|
||||
} else {
|
||||
document()
|
||||
.create_element("div")
|
||||
.expect("HTML element creation to work")
|
||||
};
|
||||
|
||||
let render_root = if use_shadow {
|
||||
container
|
||||
|
||||
@@ -10,18 +10,18 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
config = { version = "0.15.8", default-features = false, features = [
|
||||
config = { version = "0.15.11", default-features = false, features = [
|
||||
"toml",
|
||||
"convert-case",
|
||||
] }
|
||||
regex = "1.11"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
thiserror = { workspace = true }
|
||||
typed-builder = { workspace = true }
|
||||
serde = { features = ["derive", "rc"] , workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
typed-builder = { workspace = true , default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.43", features = ["rt", "macros"] }
|
||||
tempfile = "3.14"
|
||||
tokio = { features = ["rt", "macros"] , workspace = true, default-features = true }
|
||||
tempfile = "3.19"
|
||||
temp-env = { version = "0.3.6", features = ["async_closure"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -12,19 +12,20 @@ edition.workspace = true
|
||||
tachys = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
js-sys = "0.3.74"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
js-sys = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true , default-features = true }
|
||||
serde_json = { optional = true , workspace = true, default-features = true }
|
||||
serde = { optional = true , workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.72"
|
||||
features = ["Location"]
|
||||
workspace = true
|
||||
default-features = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -11,18 +11,18 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
syn = { version = "2.0", features = [
|
||||
serde = { features = ["derive"] , workspace = true, default-features = true }
|
||||
syn = { features = [
|
||||
"full",
|
||||
"parsing",
|
||||
"extra-traits",
|
||||
"visit",
|
||||
"printing",
|
||||
] }
|
||||
quote = "1.0"
|
||||
rstml = "0.12.0"
|
||||
proc-macro2 = { version = "1.0", features = ["span-locations", "nightly"] }
|
||||
parking_lot = "0.12.3"
|
||||
] , workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
rstml = { workspace = true, default-features = true }
|
||||
proc-macro2 = { features = ["span-locations", "nightly"] , workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
walkdir = "2.5"
|
||||
camino = "1.1"
|
||||
indexmap = "2.6"
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
|
||||
@@ -14,38 +14,38 @@ proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
attribute-derive = { version = "0.10.3", features = ["syn-full"] }
|
||||
cfg-if = "1.0"
|
||||
html-escape = "0.2.13"
|
||||
itertools = { workspace = true }
|
||||
prettyplease = "0.2.25"
|
||||
proc-macro-error2 = { version = "2.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
rstml = "0.12.0"
|
||||
cfg-if = { workspace = true, default-features = true }
|
||||
html-escape = { workspace = true, default-features = true }
|
||||
itertools = { workspace = true , default-features = true }
|
||||
prettyplease = "0.2.32"
|
||||
proc-macro-error2 = { default-features = false , workspace = true }
|
||||
proc-macro2 = { workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
syn = { features = ["full"] , workspace = true, default-features = true }
|
||||
rstml = { workspace = true, default-features = true }
|
||||
leptos_hot_reload = { workspace = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
convert_case = { workspace = true }
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
convert_case = { workspace = true , default-features = true }
|
||||
uuid = { version = "1.16", features = ["v4"] }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4.22"
|
||||
typed-builder = "0.20.0"
|
||||
trybuild = { workspace = true }
|
||||
log = "0.4.27"
|
||||
typed-builder = { workspace = true, default-features = true }
|
||||
trybuild = { workspace = true , default-features = true }
|
||||
leptos = { path = "../leptos" }
|
||||
leptos_router = { path = "../router", features = ["ssr"] }
|
||||
server_fn = { path = "../server_fn", features = ["cbor"] }
|
||||
insta = "1.41"
|
||||
serde = "1.0"
|
||||
insta = "1.42"
|
||||
serde = { workspace = true, default-features = true }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
csr = []
|
||||
hydrate = []
|
||||
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
|
||||
ssr = ["server_fn_macro/ssr"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = ["dep:tracing"]
|
||||
islands = []
|
||||
|
||||
@@ -359,23 +359,11 @@ impl ToTokens for Model {
|
||||
let component = if is_island {
|
||||
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
|
||||
quote! {
|
||||
{
|
||||
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
|
||||
.map(|h| h.0)
|
||||
.unwrap_or(false) {
|
||||
::leptos::either::Either::Left(
|
||||
#component
|
||||
)
|
||||
} else {
|
||||
::leptos::either::Either::Right(
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
stringify!(#hydrate_fn_name),
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
)
|
||||
}
|
||||
}
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
stringify!(#hydrate_fn_name),
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
}
|
||||
} else {
|
||||
component
|
||||
@@ -646,7 +634,13 @@ impl Parse for DummyModel {
|
||||
let mut attrs = input.call(Attribute::parse_outer)?;
|
||||
// Drop unknown attributes like #[deprecated]
|
||||
drain_filter(&mut attrs, |attr| {
|
||||
!(attr.path().is_ident("doc") || attr.path().is_ident("allow"))
|
||||
let path = attr.path();
|
||||
!(path.is_ident("doc")
|
||||
|| path.is_ident("allow")
|
||||
|| path.is_ident("expect")
|
||||
|| path.is_ident("warn")
|
||||
|| path.is_ident("deny")
|
||||
|| path.is_ident("forbid"))
|
||||
});
|
||||
|
||||
let vis: Visibility = input.parse()?;
|
||||
@@ -935,13 +929,20 @@ impl UnknownAttrs {
|
||||
let attrs = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
if attr.path().is_ident("doc") {
|
||||
let path = attr.path();
|
||||
|
||||
if path.is_ident("doc") {
|
||||
if let Meta::NameValue(_) = &attr.meta {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if attr.path().is_ident("allow") {
|
||||
if path.is_ident("allow")
|
||||
|| path.is_ident("expect")
|
||||
|| path.is_ident("warn")
|
||||
|| path.is_ident("deny")
|
||||
|| path.is_ident("forbid")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -409,6 +409,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// generate documentation for the component.
|
||||
///
|
||||
/// Here’s how you would define and use a simple Leptos component which can accept custom properties for a name and age:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::prelude::*;
|
||||
/// use std::time::Duration;
|
||||
@@ -446,6 +447,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
///
|
||||
/// Here are some important details about how Leptos components work within the framework:
|
||||
///
|
||||
/// * **The component function only runs once.** Your component function is not a “render” function
|
||||
/// that re-runs whenever changes happen in the state. It’s a “setup” function that runs once to
|
||||
/// create the user interface, and sets up a reactive system to update it. This means it’s okay
|
||||
@@ -458,7 +460,6 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// // PascalCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
/// fn MyComponent() -> impl IntoView {}
|
||||
@@ -500,8 +501,10 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
///
|
||||
/// ## Customizing Properties
|
||||
///
|
||||
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
|
||||
/// customize the types that component property can receive. You can use the following attributes:
|
||||
///
|
||||
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
|
||||
/// you could apply `#[prop(into)]` to a prop that takes
|
||||
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
|
||||
@@ -514,6 +517,11 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// * `#[prop(optional_no_strip)]`: The same as `optional`, but requires values to be passed as `None` or
|
||||
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
|
||||
/// specified as either `None` or `Some(T)`.
|
||||
/// * `#[prop(default = <expr>)]`: Optional property that specifies a default value, which is used when the
|
||||
/// property is not specified.
|
||||
/// * `#[prop(name = "new_name")]`: Specifiy a different name for the property. Can be used to destructure
|
||||
/// fields in component function parameters (see example below).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
@@ -522,6 +530,8 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// #[prop(into)] name: String,
|
||||
/// #[prop(optional)] optional_value: Option<i32>,
|
||||
/// #[prop(optional_no_strip)] optional_no_strip: Option<i32>,
|
||||
/// #[prop(default = 7)] optional_default: i32,
|
||||
/// #[prop(name = "data")] UserInfo { email, user_id }: UserInfo,
|
||||
/// ) -> impl IntoView {
|
||||
/// // whatever UI you need
|
||||
/// }
|
||||
@@ -530,16 +540,24 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
|
||||
/// pub fn App() -> impl IntoView {
|
||||
/// view! {
|
||||
/// <MyComponent
|
||||
/// name="Greg" // automatically converted to String with `.into()`
|
||||
/// optional_value=42 // received as `Some(42)`
|
||||
/// optional_no_strip=Some(42) // received as `Some(42)`
|
||||
/// name="Greg" // automatically converted to String with `.into()`
|
||||
/// optional_value=42 // received as `Some(42)`
|
||||
/// optional_no_strip=Some(42) // received as `Some(42)`
|
||||
/// optional_default=42 // received as `42`
|
||||
/// data=UserInfo {email: "foo", user_id: "bar" }
|
||||
/// />
|
||||
/// <MyComponent
|
||||
/// name="Bob" // automatically converted to String with `.into()`
|
||||
/// // optional values can both be omitted, and received as `None`
|
||||
/// data=UserInfo {email: "foo", user_id: "bar" }
|
||||
/// // optional values can be omitted
|
||||
/// />
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub struct UserInfo {
|
||||
/// pub email: &'static str,
|
||||
/// pub user_id: &'static str,
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_error2::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
|
||||
@@ -10,24 +10,24 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
base64 = { workspace = true, default-features = true }
|
||||
codee = { version = "0.3.0", features = ["json_serde"] }
|
||||
hydration_context = { workspace = true }
|
||||
reactive_graph = { workspace = true, features = ["hydration"] }
|
||||
server_fn = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
futures = "0.3.31"
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
|
||||
any_spawner = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
|
||||
send_wrapper = "0.6"
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
|
||||
# serialization formats
|
||||
serde = { version = "1.0" }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, default-features = true }
|
||||
js-sys = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, optional = true , default-features = true }
|
||||
serde_json = { workspace = true , default-features = true }
|
||||
|
||||
[features]
|
||||
ssr = []
|
||||
|
||||
@@ -14,11 +14,13 @@ use reactive_graph::{
|
||||
ArcRwSignal, RwSignal,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
|
||||
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
|
||||
Update, With, Write,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
future::{pending, Future, IntoFuture},
|
||||
ops::{Deref, DerefMut},
|
||||
panic::Location,
|
||||
};
|
||||
|
||||
@@ -41,6 +43,14 @@ impl<T> Clone for ArcLocalResource<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for ArcLocalResource<T> {
|
||||
type Target = ArcAsyncDerived<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ArcLocalResource<T> {
|
||||
/// Creates the resource.
|
||||
///
|
||||
@@ -62,7 +72,7 @@ impl<T> ArcLocalResource<T> {
|
||||
pending().await
|
||||
} else {
|
||||
// LocalResources that are immediately available can cause a hydration error,
|
||||
// because the future *looks* like it is alredy ready (and therefore would
|
||||
// because the future *looks* like it is already ready (and therefore would
|
||||
// already have been rendered to html on the server), but in fact was ignored
|
||||
// on the server. the simplest way to avoid this is to ensure that we always
|
||||
// wait a tick before resolving any value for a localresource.
|
||||
@@ -157,6 +167,32 @@ impl<T> DefinedAt for ArcLocalResource<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Notify for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn notify(&self) {
|
||||
self.data.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Write for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.data.try_write()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.data.try_write_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadUntracked for ArcLocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -241,6 +277,14 @@ pub struct LocalResource<T> {
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<T> Deref for LocalResource<T> {
|
||||
type Target = AsyncDerived<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for LocalResource<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
@@ -270,7 +314,7 @@ impl<T> LocalResource<T> {
|
||||
pending().await
|
||||
} else {
|
||||
// LocalResources that are immediately available can cause a hydration error,
|
||||
// because the future *looks* like it is alredy ready (and therefore would
|
||||
// because the future *looks* like it is already ready (and therefore would
|
||||
// already have been rendered to html on the server), but in fact was ignored
|
||||
// on the server. the simplest way to avoid this is to ensure that we always
|
||||
// wait a tick before resolving any value for a localresource.
|
||||
@@ -300,6 +344,34 @@ impl<T> LocalResource<T> {
|
||||
pub fn refetch(&self) {
|
||||
self.refetch.try_update(|n| *n += 1);
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.data.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> LocalResource<Result<T, E>>
|
||||
where
|
||||
T: 'static,
|
||||
E: Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoFuture for LocalResource<T>
|
||||
@@ -336,6 +408,32 @@ impl<T> DefinedAt for LocalResource<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Notify for LocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn notify(&self) {
|
||||
self.data.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Write for LocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.data.try_write()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.data.try_write_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReadUntracked for LocalResource<T>
|
||||
where
|
||||
T: 'static,
|
||||
|
||||
@@ -26,7 +26,7 @@ use reactive_graph::{
|
||||
};
|
||||
use std::{
|
||||
future::{pending, IntoFuture},
|
||||
ops::Deref,
|
||||
ops::{Deref, DerefMut},
|
||||
panic::Location,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -162,6 +162,32 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Notify for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn notify(&self) {
|
||||
self.data.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Write for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.data.try_write()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.data.try_write_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for ArcResource<T, Ser>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -842,6 +868,32 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Notify for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
fn notify(&self) {
|
||||
self.data.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> Write for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
self.data.try_write()
|
||||
}
|
||||
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.data.try_write_untracked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ReadUntracked for Resource<T, Ser>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.8.0-rc3"
|
||||
version = "0.8.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
@@ -10,17 +10,18 @@ edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { workspace = true }
|
||||
once_cell = "1.20"
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
or_poisoned = { workspace = true }
|
||||
indexmap = "2.6"
|
||||
send_wrapper = "0.6.0"
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true , default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.72"
|
||||
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
workspace = true
|
||||
default-features = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -216,6 +216,13 @@ impl ServerMetaContextOutput {
|
||||
self,
|
||||
mut stream: impl Stream<Item = String> + Send + Unpin,
|
||||
) -> impl Stream<Item = String> + Send {
|
||||
// if the first chunk consists of a synchronously-available Suspend,
|
||||
// inject_meta_context can accidentally run a tick before it, but the Suspend
|
||||
// when both are available. waiting a tick before awaiting the first chunk
|
||||
// in the Stream ensures that this always runs after that first chunk
|
||||
// see https://github.com/leptos-rs/leptos/issues/3976 for the original issue
|
||||
leptos::task::tick().await;
|
||||
|
||||
// wait for the first chunk of the stream, to ensure our components hve run
|
||||
let mut first_chunk = stream.next().await.unwrap_or_default();
|
||||
|
||||
@@ -242,23 +249,22 @@ impl ServerMetaContextOutput {
|
||||
let head_loc = first_chunk
|
||||
.find("</head>")
|
||||
.expect("you are using leptos_meta without a </head> tag");
|
||||
let marker_loc =
|
||||
first_chunk.find("<!--HEAD-->").unwrap_or_else(|| {
|
||||
let marker_loc = first_chunk
|
||||
.find("<!--HEAD-->")
|
||||
.map(|pos| pos + "<!--HEAD-->".len())
|
||||
.unwrap_or_else(|| {
|
||||
first_chunk.find("</head>").unwrap_or(head_loc)
|
||||
});
|
||||
let (before_marker, after_marker) =
|
||||
first_chunk.split_at_mut(marker_loc);
|
||||
let (before_head_close, after_head) =
|
||||
after_marker.split_at_mut(head_loc - marker_loc);
|
||||
buf.push_str(before_marker);
|
||||
buf.push_str(&meta_buf);
|
||||
if let Some(title) = title {
|
||||
buf.push_str("<title>");
|
||||
buf.push_str(&title);
|
||||
buf.push_str("</title>");
|
||||
}
|
||||
buf.push_str(before_head_close);
|
||||
buf.push_str(&meta_buf);
|
||||
buf.push_str(after_head);
|
||||
buf.push_str(after_marker);
|
||||
buf
|
||||
};
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
serde_json = { workspace = true , default-features = true }
|
||||
|
||||
@@ -19,9 +19,9 @@ leptos_router = { path = "../../router" }
|
||||
log = "0.4.0"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.0", optional = true, features = ["macros"] }
|
||||
tower = { version = "0.4.0", optional = true }
|
||||
tower-http = { version = "0.5.0", features = ["fs"], optional = true }
|
||||
axum = { version = "0.8.0", optional = true, features = ["macros"] }
|
||||
tower = { version = "0.5.0", optional = true }
|
||||
tower-http = { version = "0.6.0", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.0", features = ["full"], optional = true }
|
||||
http = { version = "1.0" }
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
@@ -30,10 +30,13 @@ sqlx = { version = "0.8.0", features = [
|
||||
], optional = true }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2.0"
|
||||
axum_session_auth = { version = "0.14.0", features = [], optional = true }
|
||||
axum_session = { version = "0.14.0", features = [], optional = true }
|
||||
axum_session_sqlx = { version = "0.3.0", features = [ "sqlite", "tls-rustls"], optional = true }
|
||||
bcrypt = { version = "0.15.0", optional = true }
|
||||
axum_session_auth = { version = "0.16.0", features = [], optional = true }
|
||||
axum_session = { version = "0.16.0", features = [], optional = true }
|
||||
axum_session_sqlx = { version = "0.5.0", features = [
|
||||
"sqlite",
|
||||
"tls-rustls",
|
||||
], optional = true }
|
||||
bcrypt = { version = "0.17.0", optional = true }
|
||||
async-trait = { version = "0.1.0", optional = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -185,7 +185,7 @@ pub async fn foo() -> Result<String, ServerFnError> {
|
||||
pub async fn get_user() -> Result<Option<User>, ServerFnError> {
|
||||
use crate::todo::ssr::auth;
|
||||
|
||||
let auth = auth()?;
|
||||
let auth = auth().await?;
|
||||
|
||||
Ok(auth.current_user)
|
||||
}
|
||||
@@ -199,7 +199,7 @@ pub async fn login(
|
||||
use self::ssr::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let auth = auth()?;
|
||||
let auth = auth().await?;
|
||||
|
||||
let (user, UserPasshash(expected_passhash)) =
|
||||
User::get_from_username_with_passhash(username, &pool)
|
||||
@@ -229,7 +229,7 @@ pub async fn signup(
|
||||
use self::ssr::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let auth = auth()?;
|
||||
let auth = auth().await?;
|
||||
|
||||
if password != password_confirmation {
|
||||
return Err(ServerFnError::ServerError(
|
||||
@@ -264,7 +264,7 @@ pub async fn signup(
|
||||
pub async fn logout() -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
let auth = auth()?;
|
||||
let auth = auth().await?;
|
||||
|
||||
auth.logout_user();
|
||||
leptos_axum::redirect("/");
|
||||
|
||||
@@ -1,62 +1,12 @@
|
||||
use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum::Router;
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer};
|
||||
use axum_session_sqlx::SessionSqlitePool;
|
||||
use leptos::{
|
||||
config::get_configuration, logging::log, prelude::provide_context,
|
||||
};
|
||||
use leptos_axum::{
|
||||
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
|
||||
};
|
||||
use session_auth_axum::{
|
||||
auth::{ssr::AuthSession, User},
|
||||
state::AppState,
|
||||
todo::*,
|
||||
};
|
||||
use leptos::{config::get_configuration, logging::log};
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use session_auth_axum::{auth::User, state::AppState, todo::*};
|
||||
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
|
||||
|
||||
async fn server_fn_handler(
|
||||
State(app_state): State<AppState>,
|
||||
auth_session: AuthSession,
|
||||
path: Path<String>,
|
||||
request: Request<AxumBody>,
|
||||
) -> impl IntoResponse {
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
state: State<AppState>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let State(app_state) = state.clone();
|
||||
let handler = leptos_axum::render_route_with_context(
|
||||
app_state.routes.clone(),
|
||||
move || {
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || shell(app_state.leptos_options.clone()),
|
||||
);
|
||||
handler(state, req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info)
|
||||
@@ -82,18 +32,6 @@ async fn main() {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = GetTodos::register();
|
||||
// _ = AddTodo::register();
|
||||
// _ = DeleteTodo::register();
|
||||
// _ = Login::register();
|
||||
// _ = Logout::register();
|
||||
// _ = Signup::register();
|
||||
// _ = GetUser::register();
|
||||
// _ = Foo::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
@@ -108,11 +46,10 @@ async fn main() {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/*fn_name",
|
||||
get(server_fn_handler).post(server_fn_handler),
|
||||
)
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
|
||||
.leptos_routes(&app_state, routes, {
|
||||
let options = app_state.leptos_options.clone();
|
||||
move || shell(options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
|
||||
.layer(
|
||||
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{auth::*, error_template::ErrorTemplate};
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::{components::*, *};
|
||||
use leptos_router::{components::*, path};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -16,19 +16,21 @@ pub struct Todo {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr {
|
||||
use super::Todo;
|
||||
use crate::auth::{ssr::AuthSession, User};
|
||||
use crate::{
|
||||
auth::{ssr::AuthSession, User},
|
||||
state::AppState,
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
with_context::<AppState, _>(|state| state.pool.clone())
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>().ok_or_else(|| {
|
||||
ServerFnError::ServerError("Auth session missing.".into())
|
||||
})
|
||||
pub async fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
let auth = leptos_axum::extract().await?;
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
@@ -165,7 +167,7 @@ pub fn TodoApp() -> impl IntoView {
|
||||
", "
|
||||
<A href="/login">"Login"</A>
|
||||
", "
|
||||
<span>{format!("Login error: {}", e)}</span>
|
||||
<span>{format!("Login error: {e}")}</span>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_graph"
|
||||
version = "0.2.0-rc3"
|
||||
version = "0.2.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -12,28 +12,28 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
any_spawner = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
futures = "0.3.31"
|
||||
futures = { workspace = true, default-features = true }
|
||||
hydration_context = { workspace = true, optional = true }
|
||||
pin-project-lite = "0.2.15"
|
||||
rustc-hash = "2.0"
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
slotmap = "1.0"
|
||||
thiserror = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
guardian = "1.2"
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
serde = { features = ["derive"], optional = true , workspace = true, default-features = true }
|
||||
slotmap = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
guardian = { workspace = true, default-features = true }
|
||||
async-lock = "3.4.0"
|
||||
send_wrapper = { version = "0.6.0", features = ["futures"] }
|
||||
send_wrapper = { features = ["futures"] , workspace = true, default-features = true }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
web-sys = { version = "0.3.72", features = ["console"] }
|
||||
web-sys = { version = "0.3.77", features = ["console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
nightly = []
|
||||
|
||||
@@ -234,7 +234,8 @@ macro_rules! spawn_derived {
|
||||
subscribers: SubscriberSet::new(),
|
||||
state: AsyncDerivedState::Clean,
|
||||
version: 0,
|
||||
suspenses: Vec::new()
|
||||
suspenses: Vec::new(),
|
||||
pending_suspenses: Vec::new()
|
||||
}));
|
||||
let value = Arc::new(AsyncRwLock::new($initial));
|
||||
let wakers = Arc::new(RwLock::new(Vec::new()));
|
||||
@@ -364,7 +365,7 @@ macro_rules! spawn_derived {
|
||||
// generate and assign new value
|
||||
loading.store(true, Ordering::Relaxed);
|
||||
|
||||
let (this_version, suspense_ids) = {
|
||||
let this_version = {
|
||||
let mut guard = inner.write().or_poisoned();
|
||||
guard.version += 1;
|
||||
let version = guard.version;
|
||||
@@ -372,14 +373,17 @@ macro_rules! spawn_derived {
|
||||
.into_iter()
|
||||
.map(|sc| sc.task_id())
|
||||
.collect::<Vec<_>>();
|
||||
(version, suspense_ids)
|
||||
guard.pending_suspenses.extend(suspense_ids);
|
||||
version
|
||||
};
|
||||
|
||||
let new_value = fut.await;
|
||||
|
||||
drop(suspense_ids);
|
||||
|
||||
let latest_version = inner.read().or_poisoned().version;
|
||||
let latest_version = {
|
||||
let mut guard = inner.write().or_poisoned();
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
guard.version
|
||||
};
|
||||
|
||||
if latest_version == this_version {
|
||||
Self::set_inner_value(new_value, value, wakers, inner, loading, Some(ready_tx)).await;
|
||||
@@ -514,7 +518,9 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||
{
|
||||
let fun = move || {
|
||||
let fut = fun();
|
||||
async move { SendOption::new(Some(fut.await)) }
|
||||
ScopedFuture::new_untracked(async move {
|
||||
SendOption::new(Some(fut.await))
|
||||
})
|
||||
};
|
||||
let initial_value = SendOption::new(initial_value);
|
||||
let (this, _) = spawn_derived!(
|
||||
@@ -641,6 +647,14 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
|
||||
// increment the version, such that a rerun triggered previously does not overwrite this
|
||||
// new value
|
||||
let mut guard = self.inner.write().or_poisoned();
|
||||
guard.version += 1;
|
||||
|
||||
// tell any suspenses to stop waiting for this
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
|
||||
Some(MappedMut::new(
|
||||
WriteGuard::new(self.clone(), self.value.blocking_write()),
|
||||
|v| v.deref(),
|
||||
@@ -651,6 +665,14 @@ impl<T: 'static> Write for ArcAsyncDerived<T> {
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
// increment the version, such that a rerun triggered previously does not overwrite this
|
||||
// new value
|
||||
let mut guard = self.inner.write().or_poisoned();
|
||||
guard.version += 1;
|
||||
|
||||
// tell any suspenses to stop waiting for this
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
|
||||
Some(MappedMut::new(
|
||||
self.value.blocking_write(),
|
||||
|v| v.deref(),
|
||||
|
||||
@@ -14,8 +14,10 @@ use crate::{
|
||||
unwrap_signal,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
future::Future,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
panic::Location,
|
||||
};
|
||||
@@ -27,7 +29,7 @@ use std::{
|
||||
/// values that depend on it that it has changed.
|
||||
///
|
||||
/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
|
||||
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas
|
||||
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
|
||||
/// as long as a reference to it is alive, see [`ArcAsyncDerived`].
|
||||
///
|
||||
/// ## Examples
|
||||
@@ -349,6 +351,17 @@ where
|
||||
let guard = self
|
||||
.inner
|
||||
.try_with_value(|n| n.value.blocking_write_arc())?;
|
||||
|
||||
self.inner.try_with_value(|n| {
|
||||
let mut guard = n.inner.write().or_poisoned();
|
||||
// increment the version, such that a rerun triggered previously does not overwrite this
|
||||
// new value
|
||||
guard.version += 1;
|
||||
|
||||
// tell any suspenses to stop waiting for this
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
});
|
||||
|
||||
Some(MappedMut::new(
|
||||
WriteGuard::new(*self, guard),
|
||||
|v| v.deref(),
|
||||
@@ -359,6 +372,16 @@ where
|
||||
fn try_write_untracked(
|
||||
&self,
|
||||
) -> Option<impl DerefMut<Target = Self::Value>> {
|
||||
self.inner.try_with_value(|n| {
|
||||
let mut guard = n.inner.write().or_poisoned();
|
||||
// increment the version, such that a rerun triggered previously does not overwrite this
|
||||
// new value
|
||||
guard.version += 1;
|
||||
|
||||
// tell any suspenses to stop waiting for this
|
||||
drop(mem::take(&mut guard.pending_suspenses));
|
||||
});
|
||||
|
||||
self.inner
|
||||
.try_with_value(|n| n.value.blocking_write_arc())
|
||||
.map(|inner| {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::suspense::TaskHandle;
|
||||
use crate::{
|
||||
channel::Sender,
|
||||
computed::suspense::SuspenseContext,
|
||||
@@ -22,6 +23,7 @@ pub(crate) struct ArcAsyncDerivedInner {
|
||||
pub state: AsyncDerivedState,
|
||||
pub version: usize,
|
||||
pub suspenses: Vec<SuspenseContext>,
|
||||
pub pending_suspenses: Vec<TaskHandle>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -42,6 +42,18 @@ impl<Fut> ScopedFuture<Fut> {
|
||||
fut,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps the given `Future` by taking the current [`Owner`] re-setting it as the
|
||||
/// active owner every time the inner `Future` is polled. Always untracks, i.e., clears
|
||||
/// the active [`Observer`] when polled.
|
||||
pub fn new_untracked(fut: Fut) -> Self {
|
||||
let owner = Owner::current().unwrap_or_default();
|
||||
Self {
|
||||
owner,
|
||||
observer: None,
|
||||
fut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut: Future> Future for ScopedFuture<Fut> {
|
||||
|
||||
@@ -15,7 +15,7 @@ pub struct MemoInner<T, S>
|
||||
where
|
||||
S: Storage<T>,
|
||||
{
|
||||
/// Must always be aquired *after* the reactivity lock
|
||||
/// Must always be acquired *after* the reactivity lock
|
||||
pub(crate) value: Arc<RwLock<Option<S::Wrapped>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fun: Arc<dyn Fn(Option<T>) -> (T, bool) + Send + Sync>,
|
||||
@@ -137,7 +137,7 @@ where
|
||||
})
|
||||
});
|
||||
|
||||
// Two locks are aquired, so order matters.
|
||||
// Two locks are acquired, so order matters.
|
||||
let reactivity_lock = self.reactivity.write().or_poisoned();
|
||||
{
|
||||
// Safety: Can block endlessly if the user is has a ReadGuard on the value
|
||||
|
||||
@@ -27,7 +27,7 @@ use std::{fmt::Debug, hash::Hash, panic::Location};
|
||||
/// not re-run the calculation when a source signal changes until they are read again.
|
||||
///
|
||||
/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive
|
||||
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas
|
||||
/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives as
|
||||
/// as long as a reference to it is alive, see [`ArcMemo`].
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct SpecialNonReactiveZone;
|
||||
|
||||
/// Exits the "special non-reactive zone" when dropped.
|
||||
#[derive(Debug)]
|
||||
pub struct SpecialNonReactiveZoneGuard;
|
||||
pub struct SpecialNonReactiveZoneGuard(bool);
|
||||
|
||||
use pin_project_lite::pin_project;
|
||||
use std::{
|
||||
@@ -31,8 +31,8 @@ thread_local! {
|
||||
impl SpecialNonReactiveZone {
|
||||
/// Suppresses warnings about non-reactive accesses until the guard is dropped.
|
||||
pub fn enter() -> SpecialNonReactiveZoneGuard {
|
||||
IS_SPECIAL_ZONE.set(true);
|
||||
SpecialNonReactiveZoneGuard
|
||||
let prev = IS_SPECIAL_ZONE.replace(true);
|
||||
SpecialNonReactiveZoneGuard(prev)
|
||||
}
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "effects"))]
|
||||
@@ -48,7 +48,7 @@ impl SpecialNonReactiveZone {
|
||||
|
||||
impl Drop for SpecialNonReactiveZoneGuard {
|
||||
fn drop(&mut self) {
|
||||
IS_SPECIAL_ZONE.set(false);
|
||||
IS_SPECIAL_ZONE.set(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,15 +104,7 @@ pub mod prelude {
|
||||
#[allow(unused)]
|
||||
#[doc(hidden)]
|
||||
pub fn log_warning(text: Arguments) {
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
tracing::warn!(text);
|
||||
}
|
||||
#[cfg(all(
|
||||
not(feature = "tracing"),
|
||||
target_arch = "wasm32",
|
||||
target_os = "unknown"
|
||||
))]
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
{
|
||||
web_sys::console::warn_1(&text.to_string().into());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The reactive ownership model, which manages effect cancelation, cleanups, and arena allocation.
|
||||
//! The reactive ownership model, which manages effect cancellation, cleanups, and arena allocation.
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
use hydration_context::SharedContext;
|
||||
@@ -32,7 +32,7 @@ pub use storage::*;
|
||||
pub use stored_value::{store_value, FromLocal, StoredValue};
|
||||
|
||||
/// A reactive owner, which manages
|
||||
/// 1) the cancelation of [`Effect`](crate::effect::Effect)s,
|
||||
/// 1) the cancellation of [`Effect`](crate::effect::Effect)s,
|
||||
/// 2) providing and accessing environment data via [`provide_context`] and [`use_context`],
|
||||
/// 3) running cleanup functions defined via [`Owner::on_cleanup`], and
|
||||
/// 4) an arena storage system to provide `Copy` handles via [`ArenaItem`], which is what allows
|
||||
@@ -167,6 +167,7 @@ impl Owner {
|
||||
.map(|parent| parent.read().or_poisoned().arena.clone())
|
||||
.unwrap_or_default(),
|
||||
paused: false,
|
||||
joined_owners: Vec::new(),
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context,
|
||||
@@ -201,6 +202,7 @@ impl Owner {
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena: Default::default(),
|
||||
paused: false,
|
||||
joined_owners: Vec::new(),
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context,
|
||||
@@ -226,6 +228,7 @@ impl Owner {
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena,
|
||||
paused,
|
||||
joined_owners: Vec::new(),
|
||||
})),
|
||||
#[cfg(feature = "hydration")]
|
||||
shared_context: self.shared_context.clone(),
|
||||
@@ -455,6 +458,7 @@ pub(crate) struct OwnerInner {
|
||||
#[cfg(feature = "sandboxed-arenas")]
|
||||
arena: Arc<RwLock<ArenaMap>>,
|
||||
paused: bool,
|
||||
joined_owners: Vec<Owner>,
|
||||
}
|
||||
|
||||
impl Debug for OwnerInner {
|
||||
|
||||
@@ -3,9 +3,19 @@ use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::VecDeque,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
impl Owner {
|
||||
#[doc(hidden)]
|
||||
pub fn join_contexts(&self, other: &Owner) {
|
||||
self.inner
|
||||
.write()
|
||||
.or_poisoned()
|
||||
.joined_owners
|
||||
.push(other.clone());
|
||||
}
|
||||
|
||||
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
|
||||
self.inner
|
||||
.write()
|
||||
@@ -21,22 +31,31 @@ impl Owner {
|
||||
fn take_context<T: 'static>(&self) -> Option<T> {
|
||||
let ty = TypeId::of::<T>();
|
||||
let mut inner = self.inner.write().or_poisoned();
|
||||
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let contexts = &mut inner.contexts;
|
||||
if let Some(context) = contexts.remove(&ty) {
|
||||
context.downcast::<T>().ok().map(|n| *n)
|
||||
} else {
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let mut this_parent = this_parent.write().or_poisoned();
|
||||
let contexts = &mut this_parent.contexts;
|
||||
let value = contexts.remove(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast::<T>().ok());
|
||||
if let Some(value) = downcast {
|
||||
return Some(*value);
|
||||
} else {
|
||||
parent =
|
||||
this_parent.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let joined = inner
|
||||
.joined_owners
|
||||
.iter()
|
||||
.map(|owner| Arc::clone(&owner.inner));
|
||||
for parent in parent.into_iter().chain(joined) {
|
||||
let mut parent = Some(parent);
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let mut this_parent = this_parent.write().or_poisoned();
|
||||
let contexts = &mut this_parent.contexts;
|
||||
let value = contexts.remove(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast::<T>().ok());
|
||||
if let Some(value) = downcast {
|
||||
return Some(*value);
|
||||
} else {
|
||||
parent = this_parent
|
||||
.parent
|
||||
.as_ref()
|
||||
.and_then(|p| p.upgrade());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -49,22 +68,31 @@ impl Owner {
|
||||
) -> Option<R> {
|
||||
let ty = TypeId::of::<T>();
|
||||
let inner = self.inner.read().or_poisoned();
|
||||
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let contexts = &inner.contexts;
|
||||
let reference = if let Some(context) = contexts.get(&ty) {
|
||||
context.downcast_ref::<T>()
|
||||
} else {
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let this_parent = this_parent.read().or_poisoned();
|
||||
let contexts = &this_parent.contexts;
|
||||
let value = contexts.get(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast_ref::<T>());
|
||||
if let Some(value) = downcast {
|
||||
return Some(cb(value));
|
||||
} else {
|
||||
parent =
|
||||
this_parent.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let joined = inner
|
||||
.joined_owners
|
||||
.iter()
|
||||
.map(|owner| Arc::clone(&owner.inner));
|
||||
for parent in parent.into_iter().chain(joined) {
|
||||
let mut parent = Some(parent);
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let this_parent = this_parent.read().or_poisoned();
|
||||
let contexts = &this_parent.contexts;
|
||||
let value = contexts.get(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast_ref::<T>());
|
||||
if let Some(value) = downcast {
|
||||
return Some(cb(value));
|
||||
} else {
|
||||
parent = this_parent
|
||||
.parent
|
||||
.as_ref()
|
||||
.and_then(|p| p.upgrade());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -78,22 +106,31 @@ impl Owner {
|
||||
) -> Option<R> {
|
||||
let ty = TypeId::of::<T>();
|
||||
let mut inner = self.inner.write().or_poisoned();
|
||||
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let contexts = &mut inner.contexts;
|
||||
let reference = if let Some(context) = contexts.get_mut(&ty) {
|
||||
context.downcast_mut::<T>()
|
||||
} else {
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let mut this_parent = this_parent.write().or_poisoned();
|
||||
let contexts = &mut this_parent.contexts;
|
||||
let value = contexts.get_mut(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast_mut::<T>());
|
||||
if let Some(value) = downcast {
|
||||
return Some(cb(value));
|
||||
} else {
|
||||
parent =
|
||||
this_parent.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
||||
let joined = inner
|
||||
.joined_owners
|
||||
.iter()
|
||||
.map(|owner| Arc::clone(&owner.inner));
|
||||
for parent in parent.into_iter().chain(joined) {
|
||||
let mut parent = Some(parent);
|
||||
while let Some(ref this_parent) = parent.clone() {
|
||||
let mut this_parent = this_parent.write().or_poisoned();
|
||||
let contexts = &mut this_parent.contexts;
|
||||
let value = contexts.get_mut(&ty);
|
||||
let downcast =
|
||||
value.and_then(|context| context.downcast_mut::<T>());
|
||||
if let Some(value) = downcast {
|
||||
return Some(cb(value));
|
||||
} else {
|
||||
parent = this_parent
|
||||
.parent
|
||||
.as_ref()
|
||||
.and_then(|p| p.upgrade());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -236,11 +273,11 @@ pub fn provide_context<T: Send + Sync + 'static>(value: T) {
|
||||
///
|
||||
/// Effect::new(move |_| {
|
||||
/// // each use_context clones the value
|
||||
/// let value =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// let value = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// assert_eq!(value, "foo");
|
||||
/// let value2 =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// let value2 = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// assert_eq!(value2, "foo");
|
||||
/// });
|
||||
/// });
|
||||
@@ -284,11 +321,11 @@ pub fn use_context<T: Clone + 'static>() -> Option<T> {
|
||||
///
|
||||
/// Effect::new(move |_| {
|
||||
/// // each use_context clones the value
|
||||
/// let value =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// let value = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// assert_eq!(value, "foo");
|
||||
/// let value2 =
|
||||
/// use_context::<String>().expect("could not find i32 in context");
|
||||
/// let value2 = use_context::<String>()
|
||||
/// .expect("could not find String in context");
|
||||
/// assert_eq!(value2, "foo");
|
||||
/// });
|
||||
/// });
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
/// An optional value that can always be sent between threads, even if its inner value
|
||||
/// in the `Some(_)` case would not be threadsafe.
|
||||
///
|
||||
/// This struct can be derefenced to `Option<T>`.
|
||||
/// This struct can be dereferenced to `Option<T>`.
|
||||
///
|
||||
/// If it has been given a local (`!Send`) value, that value is wrapped in a [`SendWrapper`], which
|
||||
/// allows sending it between threads but will panic if it is accessed or updated from a
|
||||
|
||||
@@ -22,11 +22,11 @@ use std::{
|
||||
/// allowing you to read or write directly to one of its field.
|
||||
///
|
||||
/// Tracking the mapped signal tracks changes to *any* part of the signal, and updating the signal notifies
|
||||
/// and notifies *all* depenendencies of the signal. This is not a mechanism for fine-grained reactive updates
|
||||
/// and notifies *all* dependencies of the signal. This is not a mechanism for fine-grained reactive updates
|
||||
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
|
||||
/// without exposing the original type directly to users.
|
||||
pub struct ArcMappedSignal<T> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
try_read_untracked: Arc<
|
||||
@@ -44,7 +44,7 @@ pub struct ArcMappedSignal<T> {
|
||||
impl<T> Clone for ArcMappedSignal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: self.defined_at,
|
||||
try_read_untracked: self.try_read_untracked.clone(),
|
||||
try_write: self.try_write.clone(),
|
||||
@@ -67,7 +67,7 @@ impl<T> ArcMappedSignal<T> {
|
||||
U: Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
try_read_untracked: {
|
||||
let this = inner.clone();
|
||||
@@ -110,7 +110,7 @@ impl<T> ArcMappedSignal<T> {
|
||||
impl<T> Debug for ArcMappedSignal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut partial = f.debug_struct("ArcMappedSignal");
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
partial.field("defined_at", &self.defined_at);
|
||||
partial.finish()
|
||||
}
|
||||
@@ -118,11 +118,11 @@ impl<T> Debug for ArcMappedSignal<T> {
|
||||
|
||||
impl<T> DefinedAt for ArcMappedSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
@@ -224,11 +224,11 @@ where
|
||||
/// allowing you to read or write directly to one of its field.
|
||||
///
|
||||
/// Tracking the mapped signal tracks changes to *any* part of the signal, and updating the signal notifies
|
||||
/// and notifies *all* depenendencies of the signal. This is not a mechanism for fine-grained reactive updates
|
||||
/// and notifies *all* dependencies of the signal. This is not a mechanism for fine-grained reactive updates
|
||||
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
|
||||
/// without exposing the original type directly to users.
|
||||
pub struct MappedSignal<T, S = SyncStorage> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
inner: StoredValue<ArcMappedSignal<T>, S>,
|
||||
}
|
||||
@@ -246,7 +246,7 @@ impl<T> MappedSignal<T> {
|
||||
U: Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: {
|
||||
let this = ArcRwSignal::from(inner);
|
||||
@@ -269,7 +269,7 @@ impl<T> Clone for MappedSignal<T> {
|
||||
impl<T> Debug for MappedSignal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut partial = f.debug_struct("MappedSignal");
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
partial.field("defined_at", &self.defined_at);
|
||||
partial.finish()
|
||||
}
|
||||
@@ -277,11 +277,11 @@ impl<T> Debug for MappedSignal<T> {
|
||||
|
||||
impl<T> DefinedAt for MappedSignal<T> {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
@@ -352,7 +352,7 @@ where
|
||||
#[track_caller]
|
||||
fn from(value: ArcMappedSignal<T>) -> Self {
|
||||
MappedSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: StoredValue::new(value),
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc};
|
||||
/// > Each of these has a related `_untracked()` method, which updates the signal
|
||||
/// > without notifying subscribers. Untracked updates are not desirable in most
|
||||
/// > cases, as they cause “tearing” between the signal’s value and its observed
|
||||
/// > value. If you want a non-reactive container, used [`ArenaItem`] instead.
|
||||
/// > value. If you want a non-reactive container, use [`ArenaItem`] instead.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
@@ -118,7 +118,7 @@ where
|
||||
#[track_caller]
|
||||
fn from(value: ArcWriteSignal<T>) -> Self {
|
||||
WriteSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
@@ -132,7 +132,7 @@ where
|
||||
#[track_caller]
|
||||
fn from_local(value: ArcWriteSignal<T>) -> Self {
|
||||
WriteSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
|
||||
@@ -643,7 +643,7 @@ pub trait IntoInner {
|
||||
/// The type of the value contained in the signal.
|
||||
type Value;
|
||||
|
||||
/// Returns the inner value if this is the only reference to to the signal.
|
||||
/// Returns the inner value if this is the only reference to the signal.
|
||||
/// Otherwise, returns `None` and drops this reference.
|
||||
/// # Panics
|
||||
/// Panics if the inner lock is poisoned.
|
||||
|
||||
@@ -11,7 +11,8 @@ pub mod read {
|
||||
},
|
||||
signal::{
|
||||
guards::{Mapped, Plain, ReadGuard},
|
||||
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
|
||||
ArcMappedSignal, ArcReadSignal, ArcRwSignal, MappedSignal,
|
||||
ReadSignal, RwSignal,
|
||||
},
|
||||
traits::{
|
||||
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
|
||||
@@ -807,6 +808,16 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<MappedSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: MappedSignal<T>) -> Self {
|
||||
Self::derive(move || value.get())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RwSignal<T, LocalStorage>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -839,6 +850,16 @@ pub mod read {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcMappedSignal<T>> for Signal<T>
|
||||
where
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
fn from(value: ArcMappedSignal<T>) -> Self {
|
||||
MappedSignal::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ArcRwSignal<T>> for Signal<T, LocalStorage>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores"
|
||||
version = "0.2.0-rc3"
|
||||
version = "0.2.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -10,19 +10,19 @@ rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
guardian = "1.2"
|
||||
itertools = { workspace = true }
|
||||
guardian = { workspace = true, default-features = true }
|
||||
itertools = { workspace = true , default-features = true }
|
||||
or_poisoned = { workspace = true }
|
||||
paste = "1.0"
|
||||
paste = { workspace = true, default-features = true }
|
||||
reactive_graph = { workspace = true }
|
||||
rustc-hash = "2.0"
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
reactive_stores_macro = { workspace = true }
|
||||
dashmap = "6.1"
|
||||
send_wrapper = "0.6.0"
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-test = { version = "0.4.4" }
|
||||
tokio = { features = ["rt-multi-thread", "macros"] , workspace = true, default-features = true }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
|
||||
reactive_graph = { workspace = true, features = ["effects"] }
|
||||
leptos = { path = "../leptos", features = ["csr"] }
|
||||
|
||||
@@ -5,7 +5,7 @@ Stores are a data structure for nested reactivity.
|
||||
The [`reactive_graph`](https://crates.io/crates/reactive_graph) crate provides primitives for fine-grained reactivity
|
||||
via signals, memos, and effects.
|
||||
|
||||
This crate extends that reactivity to support reactive access to nested dested, without the need to create nested signals.
|
||||
This crate extends that reactivity to support reactive access to nested structs, without the need to create nested signals.
|
||||
|
||||
Using the `#[derive(Store)]` macro on a struct creates a series of getters that allow accessing each field. Individual fields
|
||||
can then be read as if they were signals. Changes to parents will notify their children, but changing one sibling field will
|
||||
|
||||
@@ -39,7 +39,7 @@ where
|
||||
#[track_caller]
|
||||
fn deref_field(self) -> DerefedField<Self> {
|
||||
DerefedField {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
inner: self,
|
||||
}
|
||||
@@ -51,7 +51,7 @@ where
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct DerefedField<S> {
|
||||
inner: S,
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
@@ -92,11 +92,11 @@ where
|
||||
<S::Value as Deref>::Target: Sized + 'static,
|
||||
{
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
|
||||
@@ -56,12 +56,12 @@
|
||||
//!
|
||||
//! # if false { // don't run effect in doctests
|
||||
//! Effect::new(move |_| {
|
||||
//! // you can access individual store withs field a getter
|
||||
//! // you can access individual store fields with a getter
|
||||
//! println!("todos: {:?}", &*store.todos().read());
|
||||
//! });
|
||||
//! # }
|
||||
//!
|
||||
//! // won't notify the effect that listen to `todos`
|
||||
//! // won't notify the effect that listens to `todos`
|
||||
//! store.todos().write().push(Todo {
|
||||
//! label: "Test".to_string(),
|
||||
//! completed: false,
|
||||
@@ -69,7 +69,7 @@
|
||||
//! ```
|
||||
//! ### Generated traits
|
||||
//! The [`Store`](macro@Store) macro generates traits for each `struct` to which it is applied. When working
|
||||
//! within a single file more module, this is not an issue. However, when working with multiple modules
|
||||
//! within a single file or module, this is not an issue. However, when working with multiple modules
|
||||
//! or files, one needs to `use` the generated traits. The general pattern is that for each `struct`
|
||||
//! named `Foo`, the macro generates a trait named `FooStoreFields`. For example:
|
||||
//! ```rust
|
||||
@@ -140,7 +140,7 @@
|
||||
//! # use reactive_stores::Store;
|
||||
//! // Needed to use at_unkeyed() on Vec
|
||||
//! use reactive_stores::StoreFieldIter;
|
||||
//! use crate::reactive_stores::StoreFieldIterator;
|
||||
//! use reactive_stores::StoreFieldIterator;
|
||||
//! use reactive_graph::traits::Read;
|
||||
//! use reactive_graph::traits::Get;
|
||||
//!
|
||||
@@ -608,6 +608,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> PartialEq for Store<T, S> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Eq for Store<T, S> {}
|
||||
|
||||
impl<T> Store<T, LocalStorage>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -759,7 +767,7 @@ where
|
||||
{
|
||||
fn from(value: ArcStore<T>) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: value.defined_at,
|
||||
inner: ArenaItem::new_with_storage(value),
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ where
|
||||
///
|
||||
/// This returns `None` if the subfield is currently `None`,
|
||||
/// and a new store subfield with the inner value if it is `Some`. This can be used in some
|
||||
/// other reactive context, which will cause it to re-run if the field toggles betwen `None`
|
||||
/// other reactive context, which will cause it to re-run if the field toggles between `None`
|
||||
/// and `Some(_)`.
|
||||
fn map<U>(
|
||||
self,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "reactive_stores_macro"
|
||||
version = "0.2.0-rc3"
|
||||
version = "0.2.2"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -13,8 +13,8 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
convert_case = { workspace = true }
|
||||
proc-macro-error2 = "2.0"
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
convert_case = { workspace = true , default-features = true }
|
||||
proc-macro-error2 = { workspace = true, default-features = true }
|
||||
proc-macro2 = { workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
syn = { features = ["full"] , workspace = true, default-features = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.8.0-rc3"
|
||||
version = "0.8.2"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -17,19 +17,18 @@ either_of = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
tachys = { workspace = true, features = ["reactive_graph"] }
|
||||
futures = "0.3.31"
|
||||
url = "2.5"
|
||||
js-sys = { version = "0.3.74" }
|
||||
wasm-bindgen = { workspace = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
once_cell = "1.20"
|
||||
send_wrapper = "0.6.0"
|
||||
thiserror = { workspace = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
url = { workspace = true, default-features = true }
|
||||
js-sys = { workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true , default-features = true }
|
||||
tracing = { optional = true , workspace = true, default-features = true }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true , default-features = true }
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
gloo-net = "0.6.0"
|
||||
gloo-net = { workspace = true, default-features = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.72"
|
||||
features = [
|
||||
"Document",
|
||||
"Window",
|
||||
@@ -55,9 +54,11 @@ features = [
|
||||
"RequestMode",
|
||||
"Response",
|
||||
]
|
||||
workspace = true
|
||||
default-features = true
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
@@ -418,7 +418,7 @@ impl RenderHtml for MatchedRoute {
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
if mark_branches {
|
||||
if mark_branches && escape {
|
||||
buf.open_branch(&self.0);
|
||||
}
|
||||
self.1.to_html_with_buf(
|
||||
@@ -428,8 +428,11 @@ impl RenderHtml for MatchedRoute {
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if mark_branches {
|
||||
if mark_branches && escape {
|
||||
buf.close_branch(&self.0);
|
||||
if *position == Position::NextChildAfterText {
|
||||
*position = Position::NextChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +446,7 @@ impl RenderHtml for MatchedRoute {
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
if mark_branches {
|
||||
if mark_branches && escape {
|
||||
buf.open_branch(&self.0);
|
||||
}
|
||||
self.1.to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||
@@ -453,8 +456,11 @@ impl RenderHtml for MatchedRoute {
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
if mark_branches {
|
||||
if mark_branches && escape {
|
||||
buf.close_branch(&self.0);
|
||||
if *position == Position::NextChildAfterText {
|
||||
*position = Position::NextChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ mod tests {
|
||||
assert!(!is_active_for("/else/where", "/", f));
|
||||
assert!(!is_active_for("/no/where/", "/", f));
|
||||
|
||||
// mismatch either side all cominations of trailing slashes
|
||||
// mismatch either side all combinations of trailing slashes
|
||||
assert!(!is_active_for("/level", "/item", f));
|
||||
assert!(!is_active_for("/level", "/item/", f));
|
||||
assert!(!is_active_for("/level/", "/item", f));
|
||||
@@ -383,7 +383,7 @@ mod tests {
|
||||
//
|
||||
// assert!(is_active_for("/", "/item", true));
|
||||
//
|
||||
// Perhaps there needs to be a flag such that aria-curently applies only the _same level_, e.g
|
||||
// Perhaps there needs to be a flag such that aria-curent applies only the _same level_, e.g
|
||||
// assert!(is_same_level("/", "/"))
|
||||
// assert!(is_same_level("/", "/anything"))
|
||||
// assert!(!is_same_level("/", "/some/"))
|
||||
|
||||
@@ -14,6 +14,22 @@ pub use static_segment::*;
|
||||
pub trait PossibleRouteMatch {
|
||||
fn optional(&self) -> bool;
|
||||
|
||||
/// Checks if this segment matches beginning of the path
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * path - unmatched reminder of the path.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// If segment doesn't match a path then returns `None`. In case of a match returns the
|
||||
/// information about which part of the path was matched.
|
||||
///
|
||||
/// 1. Paths which are empty `""` or just `"/"` should match.
|
||||
/// 2. If you match just a path `"/"`, you should preserve the starting slash
|
||||
/// in the [remaining](PartialPathMatch::remaining) part, so other segments which will be
|
||||
/// tested can detect wherever they are matching from the beginning of the given path segment.
|
||||
fn test<'a>(&self, path: &'a str) -> Option<PartialPathMatch<'a>>;
|
||||
|
||||
fn generate_path(&self, path: &mut Vec<PathSegment>);
|
||||
|
||||
@@ -80,6 +80,9 @@ impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
|
||||
{
|
||||
this.next();
|
||||
}
|
||||
} else if !path.is_empty() {
|
||||
// Path must start with `/` otherwise we are not certain about being at the beginning of the segment in the path
|
||||
return None;
|
||||
}
|
||||
|
||||
for char in test {
|
||||
@@ -112,9 +115,16 @@ impl<T: AsPath> PossibleRouteMatch for StaticSegment<T> {
|
||||
}
|
||||
|
||||
// build the match object
|
||||
// the remaining is built from the path in, with the slice moved
|
||||
// by the length of this match
|
||||
let (matched, remaining) = path.split_at(matched_len);
|
||||
let (matched, remaining) = if matched_len == 1 && path.starts_with('/')
|
||||
{
|
||||
// If only thing that matched is `/` we can't eat it, otherwise next invocation of the
|
||||
// test function will not be able to tell that we are matching from the beginning of the path segment
|
||||
("/", path)
|
||||
} else {
|
||||
// the remaining is built from the path in, with the slice moved
|
||||
// by the length of this match
|
||||
path.split_at(matched_len)
|
||||
};
|
||||
has_matched.then(|| PartialPathMatch::new(remaining, vec![], matched))
|
||||
}
|
||||
|
||||
@@ -225,6 +235,17 @@ mod tests {
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_empty_match() {
|
||||
let path = "";
|
||||
let def = StaticSegment("");
|
||||
let matched = def.test(path).expect("couldn't match route");
|
||||
assert_eq!(matched.matched(), "");
|
||||
assert_eq!(matched.remaining(), "");
|
||||
let params = matched.params();
|
||||
assert!(params.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple_static_mismatch() {
|
||||
let path = "/foo/baz";
|
||||
@@ -239,6 +260,13 @@ mod tests {
|
||||
assert!(def.test(path).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_match_smooshed_segments() {
|
||||
let path = "/foobar";
|
||||
let def = (StaticSegment(Foo), StaticSegment(Bar));
|
||||
assert!(def.test(path).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arbitrary_nesting_of_tuples_has_no_effect_on_matching() {
|
||||
let path = "/foo/bar";
|
||||
|
||||
@@ -120,6 +120,20 @@ pub trait MatchNestedRoutes {
|
||||
type Data;
|
||||
type Match: MatchInterface + MatchParams;
|
||||
|
||||
/// Matches nested routes
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * path - A path which is being navigated to
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Tuple where
|
||||
///
|
||||
/// * 0 - If match has been found `Some` containing tuple where
|
||||
/// * 0 - [RouteMatchId] identifying the matching route
|
||||
/// * 1 - [Self::Match] matching route
|
||||
/// * 1 - Remaining path
|
||||
fn match_nested<'a>(
|
||||
&'a self,
|
||||
path: &'a str,
|
||||
@@ -147,7 +161,7 @@ mod tests {
|
||||
matching::MatchParams, MatchInterface, PathSegment, StaticSegment,
|
||||
WildcardSegment,
|
||||
};
|
||||
use either_of::Either;
|
||||
use either_of::{Either, EitherOf4};
|
||||
|
||||
#[test]
|
||||
pub fn matches_single_root_route() {
|
||||
@@ -324,12 +338,38 @@ mod tests {
|
||||
let params = matched.to_params();
|
||||
assert_eq!(params, vec![("any".into(), "foobar".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn dont_match_smooshed_static_segments() {
|
||||
let routes = RouteDefs::<_>::new((
|
||||
NestedRoute::new(StaticSegment(""), || ()),
|
||||
NestedRoute::new(StaticSegment("users"), || ()),
|
||||
NestedRoute::new(
|
||||
(StaticSegment("users"), StaticSegment("id")),
|
||||
|| (),
|
||||
),
|
||||
NestedRoute::new(WildcardSegment("any"), || ()),
|
||||
));
|
||||
|
||||
let matched = routes.match_route("/users");
|
||||
assert!(matches!(matched, Some(EitherOf4::B(..))));
|
||||
|
||||
let matched = routes.match_route("/users/id");
|
||||
assert!(matches!(matched, Some(EitherOf4::C(..))));
|
||||
|
||||
let matched = routes.match_route("/usersid");
|
||||
assert!(matches!(matched, Some(EitherOf4::D(..))));
|
||||
}
|
||||
}
|
||||
|
||||
/// Successful result of [testing](PossibleRouteMatch::test) a single segment in the route path
|
||||
#[derive(Debug)]
|
||||
pub struct PartialPathMatch<'a> {
|
||||
/// unmatched yet part of the path
|
||||
pub(crate) remaining: &'a str,
|
||||
/// value of parameters encoded inside of the path
|
||||
pub(crate) params: Vec<(Cow<'static, str>, String)>,
|
||||
/// part of the original path that was matched by segment
|
||||
pub(crate) matched: &'a str,
|
||||
}
|
||||
|
||||
|
||||
@@ -473,7 +473,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
type OutletViewFn = Box<dyn FnMut() -> Suspend<AnyView> + Send>;
|
||||
type OutletViewFn = Box<dyn FnMut(Owner) -> Suspend<AnyView> + Send>;
|
||||
|
||||
pub(crate) struct RouteContext {
|
||||
id: RouteMatchId,
|
||||
@@ -624,7 +624,7 @@ where
|
||||
params,
|
||||
owner: owner.clone(),
|
||||
matched,
|
||||
view_fn: Arc::new(Mutex::new(Box::new(|| {
|
||||
view_fn: Arc::new(Mutex::new(Box::new(|_owner| {
|
||||
Suspend::new(Box::pin(async { ().into_any() }))
|
||||
}))),
|
||||
base: base.clone(),
|
||||
@@ -645,30 +645,33 @@ where
|
||||
provide_context(url);
|
||||
provide_context(matched.clone());
|
||||
view.preload().await;
|
||||
*view_fn.lock().or_poisoned() = Box::new(move || {
|
||||
let view = view.clone();
|
||||
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_untracked(),
|
||||
view,
|
||||
);
|
||||
OwnedView::new(view).into_any()
|
||||
})
|
||||
as Pin<
|
||||
Box<
|
||||
dyn Future<Output = AnyView> + Send,
|
||||
>,
|
||||
>)
|
||||
}
|
||||
})
|
||||
});
|
||||
*view_fn.lock().or_poisoned() =
|
||||
Box::new(move |owner_where_used| {
|
||||
owner.join_contexts(&owner_where_used);
|
||||
let view = view.clone();
|
||||
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_untracked(),
|
||||
view,
|
||||
);
|
||||
OwnedView::new(view).into_any()
|
||||
})
|
||||
as Pin<
|
||||
Box<
|
||||
dyn Future<Output = AnyView>
|
||||
+ Send,
|
||||
>,
|
||||
>)
|
||||
}
|
||||
})
|
||||
});
|
||||
trigger
|
||||
}
|
||||
})
|
||||
@@ -799,7 +802,8 @@ where
|
||||
provide_context(matched);
|
||||
view.preload().await;
|
||||
*view_fn.lock().or_poisoned() =
|
||||
Box::new(move || {
|
||||
Box::new(move |owner_where_used| {
|
||||
owner.join_contexts(&owner_where_used);
|
||||
let owner = owner.clone();
|
||||
let view = view.clone();
|
||||
let full_tx =
|
||||
@@ -921,6 +925,6 @@ where
|
||||
} = ctx;
|
||||
trigger.track();
|
||||
let mut view_fn = view_fn.lock().or_poisoned();
|
||||
view_fn()
|
||||
view_fn(Owner::current().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,9 @@ pub enum SsrMode {
|
||||
/// - *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
|
||||
#[default]
|
||||
OutOfOrder,
|
||||
/// **In-order streaming** (`InOrder`): Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
|
||||
/// - *Pros*: Does not require JS for HTML to appear in correct order.
|
||||
/// - *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
|
||||
/// of the page will not be interactive until the suspended chunks have loaded.
|
||||
/// **Partially-blocked out-of-order streaming** (`PartiallyBlocked`): Using `create_blocking_resource` with out-of-order streaming still sends fallbacks and relies on JavaScript to fill them in with the fragments. Partially-blocked streaming does this replacement on the server, making for a slower response but requiring no JavaScript to show blocking resources.
|
||||
/// - *Pros*: Works better if JS is disabled.
|
||||
/// - *Cons*: Slower initial response because of additional string manipulation on server.
|
||||
PartiallyBlocked,
|
||||
/// **In-order streaming** (`InOrder`): Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
|
||||
/// - *Pros*: Does not require JS for HTML to appear in correct order.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router_macro"
|
||||
version = "0.8.0-rc3"
|
||||
version = "0.8.2"
|
||||
authors = ["Greg Johnston", "Ben Wishovich"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -13,10 +13,10 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error2 = { version = "2.0", default-features = false }
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
proc-macro-error2 = { default-features = false , workspace = true }
|
||||
proc-macro2 = { workspace = true, default-features = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
syn = { features = ["full"] , workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos_router = { path = "../router" }
|
||||
|
||||
@@ -13,77 +13,86 @@ edition.workspace = true
|
||||
throw_error = { workspace = true }
|
||||
server_fn_macro_default = { workspace = true }
|
||||
# used for hashing paths in #[server] macro
|
||||
const_format = "0.2.33"
|
||||
const_format = { workspace = true, default-features = true }
|
||||
const-str = "0.6.2"
|
||||
xxhash-rust = { version = "0.8.12", features = ["const_xxh64"] }
|
||||
rustversion = { workspace = true}
|
||||
rustversion = "1.0"
|
||||
xxhash-rust = { features = [
|
||||
"const_xxh64",
|
||||
], workspace = true, default-features = true }
|
||||
# used across multiple features
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
send_wrapper = { version = "0.6.0", features = ["futures"], optional = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { features = ["derive"], workspace = true, default-features = true }
|
||||
send_wrapper = { features = [
|
||||
"futures",
|
||||
], optional = true, workspace = true, default-features = true }
|
||||
thiserror = { workspace = true, default-features = true }
|
||||
|
||||
# registration system
|
||||
inventory = { version = "0.3.15", optional = true }
|
||||
dashmap = "6.1"
|
||||
once_cell = "1.20"
|
||||
inventory = { version = "0.3.20", optional = true }
|
||||
dashmap = { workspace = true, default-features = true }
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
|
||||
## servers
|
||||
# actix
|
||||
actix-web = { version = "4.9", optional = true }
|
||||
actix-web = { optional = true, workspace = true, default-features = true }
|
||||
actix-ws = { version = "0.3.0", optional = true }
|
||||
|
||||
# axum
|
||||
axum = { version = "0.8.1", optional = true, default-features = false, features = [
|
||||
axum = { optional = true, default-features = false, features = [
|
||||
"multipart",
|
||||
] }
|
||||
tower = { version = "0.5.1", optional = true }
|
||||
], workspace = true }
|
||||
tower = { optional = true, workspace = true, default-features = true }
|
||||
tower-layer = { version = "0.3.3", optional = true }
|
||||
|
||||
## input encodings
|
||||
serde_qs = { version = "0.14.0" }
|
||||
serde_qs = { workspace = true, default-features = true }
|
||||
multer = { version = "3.1", optional = true }
|
||||
|
||||
## output encodings
|
||||
# serde
|
||||
serde_json = { workspace = true }
|
||||
serde_json = { workspace = true, default-features = true }
|
||||
serde-lite = { version = "0.5.0", features = ["derive"], optional = true }
|
||||
futures = "0.3.31"
|
||||
http = { version = "1.1" }
|
||||
futures = { workspace = true, default-features = true }
|
||||
http = { version = "1.3" }
|
||||
ciborium = { version = "0.2.2", optional = true }
|
||||
postcard = { version = "1", features = ["alloc"], optional = true }
|
||||
hyper = { version = "1.5", optional = true }
|
||||
bytes = "1.9"
|
||||
http-body-util = { version = "0.1.2", optional = true }
|
||||
rkyv = { version = "0.8.9", optional = true }
|
||||
hyper = { version = "1.6", optional = true }
|
||||
bytes = "1.10"
|
||||
http-body-util = { version = "0.1.3", optional = true }
|
||||
rkyv = { version = "0.8.10", optional = true }
|
||||
rmp-serde = { version = "1.3.0", optional = true }
|
||||
base64 = { version = "0.22.1" }
|
||||
base64 = { workspace = true, default-features = true }
|
||||
|
||||
# client
|
||||
gloo-net = { version = "0.6.0", optional = true }
|
||||
js-sys = { version = "0.3.74", optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.50", optional = true }
|
||||
gloo-net = { optional = true, workspace = true, default-features = true }
|
||||
js-sys = { optional = true, workspace = true, default-features = true }
|
||||
wasm-bindgen = { workspace = true, optional = true, default-features = true }
|
||||
wasm-bindgen-futures = { optional = true, workspace = true, default-features = true }
|
||||
wasm-streams = { version = "0.4.2", optional = true }
|
||||
web-sys = { version = "0.3.72", optional = true, features = [
|
||||
web-sys = { optional = true, features = [
|
||||
"console",
|
||||
"ReadableStream",
|
||||
"ReadableStreamDefaultReader",
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
] }
|
||||
], workspace = true, default-features = true }
|
||||
|
||||
# reqwest client
|
||||
reqwest = { version = "0.12.9", default-features = false, optional = true, features = [
|
||||
reqwest = { version = "0.12.15", default-features = false, optional = true, features = [
|
||||
"multipart",
|
||||
"stream",
|
||||
] }
|
||||
tokio-tungstenite = { version = "0.26.2", optional = true }
|
||||
url = "2"
|
||||
pin-project-lite = "0.2.15"
|
||||
tokio = { version = "1.43.0", features = ["rt"], optional = true }
|
||||
url = { workspace = true, default-features = true }
|
||||
pin-project-lite = { workspace = true, default-features = true }
|
||||
tokio = { features = [
|
||||
"rt",
|
||||
], optional = true, workspace = true, default-features = true }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = { workspace = true }
|
||||
trybuild = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
axum-no-default = [
|
||||
@@ -238,4 +247,7 @@ skip_feature_sets = [
|
||||
max_combination_size = 2
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(leptos_debuginfo)',
|
||||
'cfg(rustc_nightly)',
|
||||
] }
|
||||
|
||||
8
server_fn/build.rs
Normal file
8
server_fn/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use rustc_version::{version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
// Set cfg flags depending on release channel
|
||||
if matches!(version_meta().unwrap().channel, Channel::Nightly) {
|
||||
println!("cargo:rustc-cfg=rustc_nightly");
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ edition.workspace = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0" }
|
||||
syn = { workspace = true, default-features = true }
|
||||
server_fn_macro = { workspace = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -16,11 +16,11 @@ error[E0271]: expected `impl Future<Output = ()>` to be a future that resolves t
|
||||
|
|
||||
= note: expected enum `Result<_, _>`
|
||||
found unit type `()`
|
||||
note: required by a bound in `ServerFn::{synthetic#0}`
|
||||
note: required by a bound in `ServerFn::{anon_assoc#0}`
|
||||
--> src/lib.rs
|
||||
|
|
||||
| ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{synthetic#0}`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{anon_assoc#0}`
|
||||
|
||||
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/empty_return.rs:3:1
|
||||
|
||||
@@ -16,11 +16,11 @@ error[E0271]: expected `impl Future<Output = CustomError>` to be a future that r
|
||||
|
|
||||
= note: expected enum `Result<_, _>`
|
||||
found enum `CustomError`
|
||||
note: required by a bound in `ServerFn::{synthetic#0}`
|
||||
note: required by a bound in `ServerFn::{anon_assoc#0}`
|
||||
--> src/lib.rs
|
||||
|
|
||||
| ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{synthetic#0}`
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{anon_assoc#0}`
|
||||
|
||||
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
|
||||
--> tests/invalid/not_result.rs:25:1
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// The trybuild output has slightly different error message ouptut for
|
||||
// The trybuild output has slightly different error message output for
|
||||
// different combinations of features. Since tests are run with `test-all-features`
|
||||
// multiple combinations of features are tested. This ensures this file is only
|
||||
// run when **only** the browser feature is enabled.
|
||||
#![cfg(all(
|
||||
rustc_nightly,
|
||||
feature = "browser",
|
||||
not(any(
|
||||
feature = "postcard",
|
||||
|
||||
@@ -9,16 +9,16 @@ version = { workspace = true }
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full", "parsing", "extra-traits"] }
|
||||
proc-macro2 = "1.0"
|
||||
xxhash-rust = { version = "0.8.12", features = ["const_xxh64"] }
|
||||
const_format = "0.2.33"
|
||||
convert_case = { workspace = true }
|
||||
quote = { workspace = true, default-features = true }
|
||||
syn = { features = ["full", "parsing", "extra-traits"] , workspace = true, default-features = true }
|
||||
proc-macro2 = { workspace = true, default-features = true }
|
||||
xxhash-rust = { features = ["const_xxh64"] , workspace = true, default-features = true }
|
||||
const_format = { workspace = true, default-features = true }
|
||||
convert_case = { workspace = true , default-features = true }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -645,7 +645,7 @@ impl ServerFnCall {
|
||||
quote! {
|
||||
vec![
|
||||
#(
|
||||
std::sync::Arc::new(#middlewares),
|
||||
std::sync::Arc::new(#middlewares)
|
||||
),*
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tachys"
|
||||
version = "0.2.0-rc3"
|
||||
version = "0.2.3"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
@@ -18,16 +18,16 @@ next_tuple = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
reactive_graph = { workspace = true, optional = true }
|
||||
reactive_stores = { workspace = true, optional = true }
|
||||
slotmap = { version = "1.0", optional = true }
|
||||
slotmap = { optional = true, workspace = true, default-features = true }
|
||||
oco_ref = { workspace = true, optional = true }
|
||||
async-trait = "0.1.81"
|
||||
once_cell = "1.20"
|
||||
paste = "1.0"
|
||||
async-trait = "0.1.88"
|
||||
once_cell = { workspace = true, default-features = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
erased = "0.1.2"
|
||||
wasm-bindgen = { workspace = true }
|
||||
html-escape = "0.2.13"
|
||||
js-sys = "0.3.74"
|
||||
web-sys = { version = "0.3.72", features = [
|
||||
wasm-bindgen = { workspace = true, default-features = true }
|
||||
html-escape = { workspace = true, default-features = true }
|
||||
js-sys = { workspace = true, default-features = true }
|
||||
web-sys = { features = [
|
||||
"Window",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
@@ -148,29 +148,32 @@ web-sys = { version = "0.3.72", features = [
|
||||
"HtmlSlotElement",
|
||||
"HtmlTemplateElement",
|
||||
"HtmlOptionElement",
|
||||
] }
|
||||
], workspace = true, default-features = true }
|
||||
drain_filter_polyfill = "0.1.3"
|
||||
indexmap = "2.6"
|
||||
rustc-hash = "2.0"
|
||||
futures = "0.3.31"
|
||||
parking_lot = "0.12.3"
|
||||
itertools = { workspace = true }
|
||||
send_wrapper = "0.6.0"
|
||||
indexmap = { workspace = true, default-features = true }
|
||||
rustc-hash = { workspace = true, default-features = true }
|
||||
futures = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
itertools = { workspace = true, default-features = true }
|
||||
send_wrapper = { workspace = true, default-features = true }
|
||||
linear-map = "1.2"
|
||||
sledgehammer_bindgen = { version = "0.6.0", features = [
|
||||
"web",
|
||||
], optional = true }
|
||||
sledgehammer_utils = { version = "0.3.1", optional = true }
|
||||
tracing = { version = "0.1.41", optional = true }
|
||||
serde = { version = "1", optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
tracing = { optional = true, workspace = true, default-features = true }
|
||||
serde = { optional = true, workspace = true, default-features = true }
|
||||
serde_json = { optional = true, workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.4"
|
||||
tokio = { version = "1.43", features = ["rt", "macros"] }
|
||||
tokio-test = { workspace = true, default-features = true }
|
||||
tokio = { features = [
|
||||
"rt",
|
||||
"macros",
|
||||
], workspace = true, default-features = true }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4.1"
|
||||
rustc_version = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["testing"]
|
||||
@@ -186,6 +189,7 @@ reactive_graph = ["dep:reactive_graph", "dep:any_spawner"]
|
||||
reactive_stores = ["reactive_graph", "dep:reactive_stores"]
|
||||
sledgehammer = ["dep:sledgehammer_bindgen", "dep:sledgehammer_utils"]
|
||||
tracing = ["dep:tracing"]
|
||||
mark_branches = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing", "sledgehammer"]
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
/// Adds a custom attribute with any key-value combintion.
|
||||
/// Adds a custom attribute with any key-value combination.
|
||||
#[inline(always)]
|
||||
pub fn custom_attribute<K, V>(key: K, value: V) -> CustomAttr<K, V>
|
||||
where
|
||||
|
||||
@@ -202,6 +202,71 @@ impl<'a> AttributeValue for &'a str {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AttributeValue for Cow<'a, str> {
|
||||
type State = (crate::renderer::types::Element, Self);
|
||||
type AsyncOutput = Self;
|
||||
type Cloneable = Arc<str>;
|
||||
type CloneableOwned = Arc<str>;
|
||||
|
||||
fn html_len(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
|
||||
fn to_html(self, key: &str, buf: &mut String) {
|
||||
buf.push(' ');
|
||||
buf.push_str(key);
|
||||
buf.push_str("=\"");
|
||||
buf.push_str(&escape_attr(&self));
|
||||
buf.push('"');
|
||||
}
|
||||
|
||||
fn to_template(_key: &str, _buf: &mut String) {}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
key: &str,
|
||||
el: &crate::renderer::types::Element,
|
||||
) -> Self::State {
|
||||
// if we're actually hydrating from SSRed HTML, we don't need to set the attribute
|
||||
// if we're hydrating from a CSR-cloned <template>, we do need to set non-StaticAttr attributes
|
||||
if !FROM_SERVER {
|
||||
Rndr::set_attribute(el, key, &self);
|
||||
}
|
||||
(el.clone(), self)
|
||||
}
|
||||
|
||||
fn build(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
key: &str,
|
||||
) -> Self::State {
|
||||
Rndr::set_attribute(el, key, &self);
|
||||
(el.to_owned(), self)
|
||||
}
|
||||
|
||||
fn rebuild(self, key: &str, state: &mut Self::State) {
|
||||
let (el, prev_value) = state;
|
||||
if self != *prev_value {
|
||||
Rndr::set_attribute(el, key, &self);
|
||||
}
|
||||
*prev_value = self;
|
||||
}
|
||||
|
||||
fn into_cloneable(self) -> Self::Cloneable {
|
||||
self.into()
|
||||
}
|
||||
|
||||
fn into_cloneable_owned(self) -> Self::CloneableOwned {
|
||||
self.into()
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {}
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "nightly", rustc_nightly))]
|
||||
impl<const V: &'static str> AttributeValue
|
||||
for crate::view::static_types::Static<V>
|
||||
|
||||
@@ -473,7 +473,7 @@ impl IntoClass for Arc<str> {
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let (el, prev) = state;
|
||||
if !Arc::ptr_eq(&self, prev) {
|
||||
if self != *prev {
|
||||
Rndr::set_attribute(el, "class", &self);
|
||||
}
|
||||
*prev = self;
|
||||
|
||||
@@ -292,7 +292,7 @@ impl InnerHtmlValue for Arc<str> {
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
if !Arc::ptr_eq(&self, &state.1) {
|
||||
if self != state.1 {
|
||||
Rndr::set_inner_html(&state.0, &self);
|
||||
state.1 = self;
|
||||
}
|
||||
|
||||
@@ -329,7 +329,6 @@ where
|
||||
fn build(self) -> Self::State {
|
||||
let el = Rndr::create_element(self.tag.tag(), E::NAMESPACE);
|
||||
|
||||
let attrs = self.attributes.build(&el);
|
||||
let children = if E::SELF_CLOSING {
|
||||
None
|
||||
} else {
|
||||
@@ -337,6 +336,9 @@ where
|
||||
children.mount(&el, None);
|
||||
Some(children)
|
||||
};
|
||||
|
||||
let attrs = self.attributes.build(&el);
|
||||
|
||||
ElementState {
|
||||
el,
|
||||
attrs,
|
||||
@@ -638,6 +640,14 @@ impl<At, Ch> Mountable for ElementState<At, Ch> {
|
||||
Rndr::insert_node(parent, self.el.as_ref(), marker);
|
||||
}
|
||||
|
||||
fn try_mount(
|
||||
&mut self,
|
||||
parent: &crate::renderer::types::Element,
|
||||
marker: Option<&crate::renderer::types::Node>,
|
||||
) -> bool {
|
||||
Rndr::try_insert_node(parent, self.el.as_ref(), marker)
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
|
||||
if let Some(parent) = Rndr::get_parent(self.el.as_ref()) {
|
||||
if let Some(element) =
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
|
||||
/// An island of interactivity in an otherwise-inert HTML document.
|
||||
pub struct Island<View> {
|
||||
has_element_representation: bool,
|
||||
component: &'static str,
|
||||
props_json: String,
|
||||
view: View,
|
||||
@@ -19,6 +20,8 @@ impl<View> Island<View> {
|
||||
/// Creates a new island with the given component name.
|
||||
pub fn new(component: &'static str, view: View) -> Self {
|
||||
Island {
|
||||
has_element_representation:
|
||||
Self::should_have_element_representation(),
|
||||
component,
|
||||
props_json: String::new(),
|
||||
view,
|
||||
@@ -51,6 +54,21 @@ impl<View> Island<View> {
|
||||
buf.push_str(ISLAND_TAG);
|
||||
buf.push('>');
|
||||
}
|
||||
|
||||
/// Whether this island should be represented by an actual HTML element
|
||||
fn should_have_element_representation() -> bool {
|
||||
#[cfg(feature = "reactive_graph")]
|
||||
{
|
||||
use reactive_graph::owner::{use_context, IsHydrating};
|
||||
let already_hydrating =
|
||||
use_context::<IsHydrating>().map(|h| h.0).unwrap_or(false);
|
||||
!already_hydrating
|
||||
}
|
||||
#[cfg(not(feature = "reactive_graph"))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<View> Render for Island<View>
|
||||
@@ -83,11 +101,13 @@ where
|
||||
Self::Output<NewAttr>: RenderHtml,
|
||||
{
|
||||
let Island {
|
||||
has_element_representation,
|
||||
component,
|
||||
props_json,
|
||||
view,
|
||||
} = self;
|
||||
Island {
|
||||
has_element_representation,
|
||||
component,
|
||||
props_json,
|
||||
view: view.add_any_attr(attr),
|
||||
@@ -114,11 +134,13 @@ where
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
let Island {
|
||||
has_element_representation,
|
||||
component,
|
||||
props_json,
|
||||
view,
|
||||
} = self;
|
||||
Island {
|
||||
has_element_representation,
|
||||
component,
|
||||
props_json,
|
||||
view: view.resolve().await,
|
||||
@@ -133,7 +155,10 @@ where
|
||||
mark_branches: bool,
|
||||
extra_attrs: Vec<AnyAttribute>,
|
||||
) {
|
||||
Self::open_tag(self.component, &self.props_json, buf);
|
||||
let has_element = self.has_element_representation;
|
||||
if has_element {
|
||||
Self::open_tag(self.component, &self.props_json, buf);
|
||||
}
|
||||
self.view.to_html_with_buf(
|
||||
buf,
|
||||
position,
|
||||
@@ -141,7 +166,9 @@ where
|
||||
mark_branches,
|
||||
extra_attrs,
|
||||
);
|
||||
Self::close_tag(buf);
|
||||
if has_element {
|
||||
Self::close_tag(buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
@@ -154,9 +181,12 @@ where
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
let has_element = self.has_element_representation;
|
||||
// insert the opening tag synchronously
|
||||
let mut tag = String::new();
|
||||
Self::open_tag(self.component, &self.props_json, &mut tag);
|
||||
if has_element {
|
||||
Self::open_tag(self.component, &self.props_json, &mut tag);
|
||||
}
|
||||
buf.push_sync(&tag);
|
||||
|
||||
// streaming render for the view
|
||||
@@ -170,7 +200,9 @@ where
|
||||
|
||||
// and insert the closing tag synchronously
|
||||
tag.clear();
|
||||
Self::close_tag(&mut tag);
|
||||
if has_element {
|
||||
Self::close_tag(&mut tag);
|
||||
}
|
||||
buf.push_sync(&tag);
|
||||
}
|
||||
|
||||
@@ -179,18 +211,21 @@ where
|
||||
cursor: &Cursor,
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
if position.get() == Position::FirstChild {
|
||||
cursor.child();
|
||||
} else if position.get() == Position::NextChild {
|
||||
cursor.sibling();
|
||||
if self.has_element_representation {
|
||||
if position.get() == Position::FirstChild {
|
||||
cursor.child();
|
||||
} else if position.get() == Position::NextChild {
|
||||
cursor.sibling();
|
||||
}
|
||||
position.set(Position::FirstChild);
|
||||
}
|
||||
position.set(Position::FirstChild);
|
||||
|
||||
self.view.hydrate::<FROM_SERVER>(cursor, position)
|
||||
}
|
||||
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
Island {
|
||||
has_element_representation: self.has_element_representation,
|
||||
component: self.component,
|
||||
props_json: self.props_json,
|
||||
view: self.view.into_owned(),
|
||||
@@ -358,6 +393,7 @@ where
|
||||
} else if curr_position != Position::Current {
|
||||
cursor.sibling();
|
||||
}
|
||||
position.set(Position::NextChild);
|
||||
|
||||
if let Some(on_hydrate) = self.on_hydrate {
|
||||
use crate::{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user