Compare commits

..

39 Commits
v0.8.2 ... 4063

Author SHA1 Message Date
Greg Johnston
ceed5e40e4 feat: allow dereferencing LocalResource to an AsyncDerived (see #4063) 2025-06-13 14:28:45 -04:00
Greg Johnston
f4e0be2d59 fix: do not track anything inside the async block of a Resource (closes #4060) (#4061) 2025-06-10 21:11:29 -04:00
Josiah Parry
05f50f7d27 chore: fix checkboxes in issue template (#4031) 2025-06-08 20:55:16 -04:00
Greg Johnston
a22d6f58be fix: allow nested untracked reads without false positives (closes #4032) (#4049) 2025-06-08 20:54:37 -04:00
Greg Johnston
ff21c9cae2 fix: suppress false-positive warning when adding children to a <For/> that is not currently mounted (closes #3385) (#4050)
* fix: suppress false-positive warning when adding children to a `<For/>` that is not currently mounted (closes #3385)

* remove track_caller
2025-06-08 20:54:25 -04:00
Greg Johnston
733a353820 fix: allow multiple #[middleware] macros (closes #4029) (#4048) 2025-06-06 20:50:52 -04:00
Greg Johnston
829b07b598 Merge pull request #4033 from leptos-rs/update_session_auth
Update `session_auth_axum` example
2025-06-02 19:44:21 -04:00
Greg Johnston
0df6cd74ee feat: simplify session_auth_axumby removing custom handlers 2025-05-30 18:32:22 -04:00
Greg Johnston
1da833a0aa fix: update session_auth_axum to Axum 0.8 2025-05-30 17:30:09 -04:00
Saikat Das
f37d124d6a Fix typo (#4025) 2025-05-29 12:39:32 -07:00
benwis
5d0e683b0f Update tachys to v0.2.3 2025-05-29 12:37:21 -07:00
lcnr
f34e3a5bc9 remove unnecessary where-clauses (#4023)
they may cause tachys to break with -Znext-solver
2025-05-29 12:35:50 -07:00
martin frances
d7dd6a1109 chore: bump rkyv to 0.8.10. (#4018) 2025-05-27 21:15:59 -04:00
Greg Johnston
ff81d34084 fix: fix <select> value by ensuring HTML children are mounted before building attributes (closes #4005) (#4008) 2025-05-27 21:15:41 -04:00
Soso
40a7aba3bc feat: impl AttributeValue for Cow<'_, str> (#4013) 2025-05-27 21:15:12 -04:00
Greg Johnston
d4dcafd908 Merge pull request #4015 from leptos-rs/3042v2
Fix context issues with nesting routing
2025-05-27 21:12:58 -04:00
Greg Johnston
82ccbbf806 copy-paste errors 2025-05-25 15:32:33 -04:00
Greg Johnston
5ba45bb1ed fix: allow Outlet to access context provided in parent view (closes #3042) 2025-05-24 17:51:22 -04:00
Greg Johnston
06dfa37eee feat: allow joining two context trees 2025-05-24 17:50:51 -04:00
Nicolas Cura
e82a0bbc7f chore: handle_response_inner public to be used in custom file_and_error_handler (closes #3996) (#3998) 2025-05-23 14:51:08 -04:00
Álvaro Mondéjar Rubio
4a972fc09e Merge pull request #4003 from mondeja/document-prop-attrs
docs: document `#[prop(default = ...)]` and `#[prop(name = ...)]`
2025-05-23 14:49:59 -04:00
Greg Johnston
07cf649e3b Merge pull request #3994 from leptos-rs/3983
fix: don't use `Arc::ptr_eq` for string comparison (closes #3983)
2025-05-19 19:15:17 -04:00
Greg Johnston
0e9598b799 fix: don't assume classList is unchanged when rebuilding a class effect for the first time (#3983 part two) 2025-05-19 09:42:33 -04:00
Greg Johnston
82303d7e33 fix: don't use Arc::ptr_eq for string comparison (closes #3983) 2025-05-19 09:27:17 -04:00
Álvaro Mondéjar Rubio
c4354ac965 fix: forward missing lint attributes passed to #[component] macro (#3989) 2025-05-18 20:38:16 -04:00
Dennis Waldherr
7de550685a docs: provide error message if file hashing is enabled but no hash file is present (#3990)
Co-authored-by: Dennis Waldherr <bytekeeper@mailbox.org>
2025-05-18 20:30:53 -04:00
mskorkowski
b1f3f6023e fix: smooshed static segments no longer matches the path #3968 (#3973)
* fix: smooshed static segments no longer matches the path #3968

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-05-18 20:29:40 -04:00
Greg Johnston
c189c3a45d fix: meta tags not properly rendered inside synchronously-available Suspend (closes #3976) (#3991) 2025-05-18 20:29:14 -04:00
Eric Roman
3903867f82 Fix spelling typos. (#3965) 2025-05-17 08:49:57 +02:00
Greg Johnston
a42fa452fc feat: add missing Resource::write() and similar functions (see #3959) (#3984) 2025-05-16 09:23:28 -04:00
Saber Haj Rabiee
cd48a6ac8c fix: remove non-existent feature dep in leptos_macro (#3985) 2025-05-16 09:23:04 -04:00
Greg Johnston
34c14adcb8 fix: render identical branch structure for out-of-order and async streaming of Suspense (closes #3970) (#3977) 2025-05-15 19:44:46 -04:00
Saber Haj Rabiee
50cee1d614 chore: upgrade rand and getrandom (#3840)
* chore: update rand and getrandom

* fix: use rng instead of thread_rng

* fix: enable getrandom wasm js backend in build.rs
2025-05-15 11:17:32 +02:00
Saber Haj Rabiee
7ca691305f chore: unify all deps with min occurrences of 2 (#3854) 2025-05-14 20:34:33 -04:00
Greg Johnston
830882f330 fix: allow rebuilding Vec<_> before it is mounted (closes #3962) (#3966) 2025-05-12 15:26:05 -04:00
Scott Little
13110a35e2 fix: add namespace to g in svg portals (closes #3958) (#3960) 2025-05-09 16:44:40 -04:00
Marcus Whybrow
304dc081a2 fix: correct doc comment for SsrMode::PartiallyBlocked (closes #3963) (#3964) 2025-05-09 09:39:00 -07:00
Serhii Shliakhov
14f6bc658e fix: deprecated parameters js warning (#3956) 2025-05-09 08:04:15 -04:00
Eric Roman
09894aaca9 Remove unnecessary "crate::" prefix in a documentation example. (#3952) 2025-05-08 07:43:27 -07:00
87 changed files with 1270 additions and 870 deletions

View File

@@ -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.

685
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -45,13 +45,12 @@ 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" }
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.2" }
leptos_config = { path = "./leptos_config", version = "0.8.2" }
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
@@ -68,16 +67,54 @@ or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
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" }
rustversion = "1"
serde_json = "1.0.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" }
trybuild = "1"
typed-builder = "0.21.0"
thiserror = "2.0.12"
wasm-bindgen = "0.2.100"
# 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

View File

@@ -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 }

View File

@@ -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"

View File

@@ -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;

View File

@@ -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"]

View File

@@ -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),

View File

@@ -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.
//

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -11,11 +11,11 @@ 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 = []

View File

@@ -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
/// ```

View File

@@ -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"] }

View File

@@ -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"]
@@ -102,7 +102,7 @@ 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

View File

@@ -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\"");
}
}

View File

@@ -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
///

View File

@@ -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.

View File

@@ -55,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);
});

View File

@@ -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");

View File

@@ -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())
}
}

View File

@@ -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

View File

@@ -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]

View File

@@ -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 = []

View File

@@ -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 }

View File

@@ -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 = []

View File

@@ -634,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()?;
@@ -923,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;
}

View File

@@ -409,6 +409,7 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
/// generate documentation for the component.
///
/// Heres 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. Its a “setup” function that runs once to
/// create the user interface, and sets up a reactive system to update it. This means its 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]

View File

@@ -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 = []

View File

@@ -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.
@@ -364,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,

View File

@@ -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,

View File

@@ -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 = []

View File

@@ -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();

View File

@@ -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 }

View File

@@ -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]

View File

@@ -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("/");

View File

@@ -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(

View File

@@ -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()
}

View File

@@ -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 = []

View File

@@ -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(),

View File

@@ -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| {

View File

@@ -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)]

View File

@@ -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> {

View File

@@ -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

View File

@@ -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`].
///
/// ```

View File

@@ -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);
}
}

View File

@@ -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());
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -22,7 +22,7 @@ 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> {
@@ -224,7 +224,7 @@ 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> {

View File

@@ -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 signals 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
/// ```

View File

@@ -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.

View File

@@ -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"] }

View File

@@ -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

View File

@@ -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;
//!

View File

@@ -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,

View File

@@ -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 }

View File

@@ -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"]

View File

@@ -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/"))

View File

@@ -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>);

View File

@@ -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";

View File

@@ -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,
}

View File

@@ -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())
}
}

View File

@@ -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.

View File

@@ -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" }

View File

@@ -13,80 +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 = "0.4.1"
rustc_version = { workspace = true, default-features = true }
[dev-dependencies]
trybuild = { workspace = true }
trybuild = { workspace = true, default-features = true }
[features]
axum-no-default = [

View File

@@ -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]

View File

@@ -1,4 +1,4 @@
// 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.

View File

@@ -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]

View File

@@ -645,7 +645,7 @@ impl ServerFnCall {
quote! {
vec![
#(
std::sync::Arc::new(#middlewares),
std::sync::Arc::new(#middlewares)
),*
]
}

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.2.2"
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"]

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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) =

View File

@@ -211,18 +211,20 @@ where
let (name, mut f) = self;
// Name might've updated:
state.name = name;
let mut first_run = true;
state.effect = RenderEffect::new_with_value(
move |prev| {
let include = *f.invoke().borrow();
match prev {
Some((class_list, prev)) => {
if include {
if !prev {
if !prev || first_run {
Rndr::add_class(&class_list, name);
}
} else if prev {
Rndr::remove_class(&class_list, name);
}
first_run = false;
(class_list.clone(), include)
}
None => {

View File

@@ -615,10 +615,7 @@ macro_rules! reactive_impl {
fn add_any_attr<NewAttr: Attribute>(
self,
_attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
) -> Self::Output<NewAttr> {
todo!()
}
}

View File

@@ -189,6 +189,14 @@ where
self.state.mount(parent, marker);
}
fn try_mount(
&mut self,
parent: &crate::renderer::types::Element,
marker: Option<&crate::renderer::types::Node>,
) -> bool {
self.state.try_mount(parent, marker)
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
self.state.insert_before_this(child)
}

View File

@@ -92,6 +92,15 @@ impl Dom {
);
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
pub fn try_insert_node(
parent: &Element,
new_child: &Node,
anchor: Option<&Node>,
) -> bool {
parent.insert_before(new_child, anchor).is_ok()
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
pub fn remove_node(parent: &Element, child: &Node) -> Option<Node> {
ok_or_debug!(parent.remove_child(child), parent, "removeNode")
@@ -496,6 +505,10 @@ impl Mountable for Node {
Dom::insert_node(parent, self, marker);
}
fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
Dom::try_insert_node(parent, self, marker)
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
let parent = Dom::get_parent(self).and_then(Element::cast_from);
if let Some(parent) = parent {
@@ -519,6 +532,10 @@ impl Mountable for Text {
Dom::insert_node(parent, self, marker);
}
fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
Dom::try_insert_node(parent, self, marker)
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
let parent =
Dom::get_parent(self.as_ref()).and_then(Element::cast_from);
@@ -543,6 +560,10 @@ impl Mountable for Comment {
Dom::insert_node(parent, self, marker);
}
fn try_mount(&mut self, parent: &Element, marker: Option<&Node>) -> bool {
Dom::try_insert_node(parent, self, marker)
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
let parent =
Dom::get_parent(self.as_ref()).and_then(Element::cast_from);

View File

@@ -213,15 +213,13 @@ impl StreamBuilder {
id.push(0);
}
let replace = view.is_some();
if let Some(view) = view {
view.to_html_async_with_buf::<true>(
&mut subbuilder,
&mut position,
true,
mark_branches,
extra_attrs,
);
}
view.to_html_async_with_buf::<true>(
&mut subbuilder,
&mut position,
true,
mark_branches,
extra_attrs,
);
let chunks = subbuilder.finish().take_chunks();
let mut flattened_chunks =
VecDeque::with_capacity(chunks.len());

View File

@@ -167,7 +167,7 @@ where
if old.is_empty() {
let mut new = self.build().states;
for item in new.iter_mut() {
Rndr::mount_before(item, marker.as_ref());
Rndr::try_mount_before(item, marker.as_ref());
}
*old = new;
} else if self.is_empty() {
@@ -186,7 +186,7 @@ where
}
itertools::EitherOrBoth::Left(new) => {
let mut new_state = new.build();
Rndr::mount_before(&mut new_state, marker.as_ref());
Rndr::try_mount_before(&mut new_state, marker.as_ref());
adds.push(new_state);
}
itertools::EitherOrBoth::Right(old) => {

View File

@@ -672,7 +672,7 @@ fn apply_diff<T, VFS, V>(
Some(marker.as_ref()),
)
} else {
each_item.mount(parent, Some(marker.as_ref()));
each_item.try_mount(parent, Some(marker.as_ref()));
}
}
@@ -697,11 +697,11 @@ fn apply_diff<T, VFS, V>(
Some(marker.as_ref()),
)
} else {
item.mount(parent, Some(marker.as_ref()));
item.try_mount(parent, Some(marker.as_ref()));
}
}
DiffOpAddMode::Append => {
item.mount(parent, Some(marker.as_ref()));
item.try_mount(parent, Some(marker.as_ref()));
}
}
}

View File

@@ -319,6 +319,16 @@ pub trait Mountable {
marker: Option<&crate::renderer::types::Node>,
);
/// Mounts a node to the interface. Returns `false` if it could not be mounted.
fn try_mount(
&mut self,
parent: &crate::renderer::types::Element,
marker: Option<&crate::renderer::types::Node>,
) -> bool {
self.mount(parent, marker);
true
}
/// Inserts another `Mountable` type before this one. Returns `false` if
/// this does not actually exist in the UI (for example, `()`).
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool;
@@ -476,7 +486,7 @@ pub enum Position {
LastChild,
}
/// Declares that this type can be converted into some other type, which can be renderered.
/// Declares that this type can be converted into some other type, which can be rendered.
pub trait IntoRender {
/// The renderable type into which this type can be converted.
type Output;

View File

@@ -372,7 +372,7 @@ impl Render for Arc<str> {
fn rebuild(self, state: &mut Self::State) {
let ArcStrState { node, str } = state;
if !Arc::ptr_eq(&self, str) {
if self != *str {
Rndr::set_text(node, &self);
*str = self;
}

View File

@@ -62,10 +62,7 @@ where
fn add_any_attr<NewAttr: Attribute>(
self,
_attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml,
{
) -> Self::Output<NewAttr> {
panic!("AddAnyAttr not supported on ViewTemplate");
}
}