Compare commits

..

43 Commits
3890 ... v0.8.2

Author SHA1 Message Date
Greg Johnston
2ee4444bb4 v0.8.2 2025-05-06 14:09:35 -04:00
Luxalpa
03a1c1e7a6 fix: ensure unique style caching hashes (#3947) 2025-05-06 14:00:29 -04:00
Greg Johnston
12e49ed996 Merge pull request #3950 from leptos-rs/3945
fix: correct order of meta content relative to surrounding tags (closes #3945)
2025-05-06 13:59:28 -04:00
Greg Johnston
1e281e9e74 fix(examples): bugfix revealed a pre-existing bug with meta tags in the hackernews demo! 2025-05-06 11:49:43 -04:00
Greg Johnston
bd475f89d0 fix: correct order of meta content relative to surrounding tags (closes #3945) 2025-05-06 11:19:19 -04:00
Greg Johnston
3d91b5e90f v0.8.1 2025-05-05 21:39:43 -04:00
Greg Johnston
96d8d5218c Merge pull request #3942 from leptos-rs/3907
Some `islands_router` improvements
2025-05-05 21:33:57 -04:00
Greg Johnston
84caa35cef feat: add .map() and .and_then() on LocalResource (#3941) 2025-05-05 21:20:34 -04:00
Greg Johnston
fc8b55161c fix: remove extra marker node after text node when marking a branch (closes #3936) (#3940) 2025-05-05 21:20:16 -04:00
Greg Johnston
657052466b fix: use a runtime check rather than an unnecessary Either to determine how to render islands (see #3896; closes #3929) (#3938) 2025-05-05 19:41:29 -04:00
william light
efe8336363 reactive_stores: implement PartialEq and Eq for Store (#3915)
StoredValue also has these implemented and does the same thing.
2025-05-05 14:32:42 -04:00
Greg Johnston
770881842c fix: correctly provide context through islands to children (closes #3928) (#3933) 2025-05-05 13:00:40 -04:00
Greg Johnston
0d540ef02f fix: ensure that nested children of a RenderEffect are dropped while dropped a RenderEffect (closes #3922) (#3926) 2025-05-05 13:00:20 -04:00
Saber Haj Rabiee
dc1885ad92 feat: check the counter_isomorphic release build with the leptos_debuginfo flag (#3918) 2025-05-04 15:22:04 -04:00
Eric Roman
61bf87439a Fix some typos in the documentation/examples for reactive store. (#3924) 2025-05-03 20:50:13 -04:00
Greg Johnston
308568e520 fix(CI): prevent regreession from nightly clippy in autofix (#3917)
* fix(CI): prevent regreession from nightly clippy in autofix

* chore: format

* chore: update nightly to 2025-04-16 (proc-macro span, #3852)

* chore: improve the autofix ci workflow

* fix: adjust ServerFn macro test stderr based on nightly-2025-04-16

* fix: limit server_fn server macro trybuild tests nightly only
2025-05-03 20:47:42 -04:00
Greg Johnston
1b0f32dc4c fix: clear and re-throw errors in correct order (#3923) 2025-05-03 20:46:57 -04:00
Greg Johnston
2e0b3011d9 fix: correct issues with StaticVec::rebuild() by aligning implementation with Vec::rebuild() (closes #3906) (#3920) 2025-05-03 08:56:36 -04:00
Greg Johnston
680d4ccd07 fix: do not diff islands in islands_router mode (see #3907) 2025-05-02 21:20:16 -04:00
Greg Johnston
325f9cbe33 fix: don't handle ActionForm (et al) with the islands_router default <form> behavior (see #3907) 2025-05-02 21:12:20 -04:00
Greg Johnston
26ab392c95 fix: allow nested Suspense > ErrorBoundary > Suspense (closes #3908) (#3913) 2025-05-02 16:59:04 -04:00
Saber Haj Rabiee
3a4e2a19aa fix: limit server_fn server macro trybuild tests nightly only 2025-05-02 08:32:38 -07:00
Saber Haj Rabiee
eed3d21b40 fix: adjust ServerFn macro test stderr based on nightly-2025-04-16 2025-05-02 08:01:15 -07:00
Saber Haj Rabiee
7ae386285d chore: improve the autofix ci workflow 2025-05-02 07:27:45 -07:00
Saber Haj Rabiee
ebcc51136d chore: update nightly to 2025-04-16 (proc-macro span, #3852) 2025-05-02 07:13:34 -07:00
Saber Haj Rabiee
e10ded4fd0 chore: format 2025-05-02 06:58:01 -07:00
Saber Haj Rabiee
67be872f58 fix(CI): prevent regreession from nightly clippy in autofix 2025-05-02 06:31:39 -07:00
LeoniePhiline
e5b21ac0fc fix(docs): correct panic message in copied example code (#3911) 2025-05-02 08:14:36 -04:00
benwis
9b2e313d20 v0.8.0 2025-05-01 15:26:21 -07:00
Greg Johnston
bc79232033 feat: impl From<MappedSignal<T>> for Signal<T> (#3897) 2025-05-01 14:56:27 -04:00
Saber Haj Rabiee
a7bb2565c4 fix(examples): websocket tests fail (occasionally) second attemp (#3910)
do not check the label immediately
2025-05-01 14:54:57 -04:00
zakstucke
2e393aaca0 fix: leptos_debuginfo (#3899) 2025-05-01 14:54:24 -04:00
Greg Johnston
e37711cb85 fix: correct hydration for elements after island children (closes #3904) (#3905) 2025-05-01 09:44:40 -04:00
Greg Johnston
627b553e60 fix: prevent sibling context leakage in islands (closes #3902) (#3903) 2025-04-30 21:33:54 -04:00
Greg Johnston
e2f9aca466 Merge pull request #3901 from leptos-rs/3896
Fix some island-routing issues
2025-04-30 14:27:31 -04:00
Greg Johnston
970544ed0b fix: correctly hydrate branches inside islands when using islands-router (closes #3896) 2025-04-30 08:48:40 -04:00
Greg Johnston
0c3b3c440f chore: remove forgotten log 2025-04-30 08:36:23 -04:00
nickburlett
6578086e09 fix(examples): incorrect routes in hackernews example (closes #3892) (#3894)
* fix(examples): incorrect routes in hackernews example (closes #3892)

1. `Avoid calling category()` twice on the story type.
2. `get_static_file()` returns Err on not found, so don’t
   unconditionally `unwrap()` it

* cargo fmt

---------

Co-authored-by: Greg Johnston <greg.johnston@gmail.com>
2025-04-30 07:43:12 -04:00
Greg Johnston
113aba9666 docs: add note about file hashing in Stylesheet docs (#3898) 2025-04-29 19:03:18 -07:00
Greg Johnston
58d7475193 fix: don't render branching comments inside script/style tags 2025-04-29 20:32:56 -04:00
Greg Johnston
04d80ff8d0 chore: clippy 2025-04-29 20:31:44 -04:00
Greg Johnston
7971a2dccb fix: marking AnyView branches with out-of-order streaming (for islands_router) 2025-04-29 20:30:40 -04:00
Greg Johnston
e2ea4277bc fix(examples): broken favicons in hackernews examples (closes #3890) (#3891) 2025-04-28 09:52:15 -04:00
55 changed files with 519 additions and 271 deletions

View File

@@ -21,33 +21,19 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
with: {toolchain: "nightly-2025-04-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Install cargo-all-features
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
- name: Install jq
run: sudo apt-get install jq
- run: |
echo "Formatting the workspace"
cargo fmt --all
echo "Running Clippy against each member's features (default features included)"
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
echo "Working on member $member":
echo -e "\tdefault-features/no-features:"
# this will also run on members with no features or default features
cargo clippy --allow-dirty --fix --lib --package "$member"
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
for feature in $features; do
if [ "$feature" = "default" ]; then
continue
fi
echo -e "\tfeature $feature"
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
done
done
- name: Format the workspace
run: cargo fmt --all
- name: Clippy the workspace
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
- uses: autofix-ci/action@v1.3.1
if: ${{ always() }}
with:

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: [stable, nightly-2025-03-05]
toolchain: [stable, nightly-2025-04-16]
erased_mode: [true, false]
steps:
- name: Free Disk Space
@@ -160,6 +160,12 @@ jobs:
run: |
cd '${{ inputs.directory }}'
cargo make --no-workspace --profile=github-actions ci
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
if: contains(inputs.directory, 'counter_isomorphic')
run: |
cd '${{ inputs.directory }}'
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
- name: Clean up ${{ inputs.directory }}
if: always()
run: |

45
Cargo.lock generated
View File

@@ -263,7 +263,7 @@ dependencies = [
[[package]]
name = "any_spawner"
version = "0.3.0-rc3"
version = "0.3.0"
dependencies = [
"async-executor",
"futures",
@@ -1781,7 +1781,7 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "leptos"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"any_spawner",
"base64",
@@ -1833,7 +1833,7 @@ dependencies = [
[[package]]
name = "leptos_actix"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"actix-files",
"actix-http",
@@ -1859,7 +1859,7 @@ dependencies = [
[[package]]
name = "leptos_axum"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"any_spawner",
"axum",
@@ -1883,7 +1883,7 @@ dependencies = [
[[package]]
name = "leptos_config"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"config",
"regex",
@@ -1897,7 +1897,7 @@ dependencies = [
[[package]]
name = "leptos_dom"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"js-sys",
"leptos",
@@ -1914,7 +1914,7 @@ dependencies = [
[[package]]
name = "leptos_hot_reload"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"anyhow",
"camino",
@@ -1930,7 +1930,7 @@ dependencies = [
[[package]]
name = "leptos_integration_utils"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"futures",
"hydration_context",
@@ -1943,7 +1943,7 @@ dependencies = [
[[package]]
name = "leptos_macro"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"attribute-derive",
"cfg-if",
@@ -1963,7 +1963,7 @@ dependencies = [
"rustc_version",
"serde",
"server_fn",
"server_fn_macro 0.8.0-rc3",
"server_fn_macro 0.8.2",
"syn 2.0.100",
"tracing",
"trybuild",
@@ -1973,7 +1973,7 @@ dependencies = [
[[package]]
name = "leptos_meta"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"futures",
"indexmap",
@@ -1988,7 +1988,7 @@ dependencies = [
[[package]]
name = "leptos_router"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"any_spawner",
"either_of",
@@ -2013,7 +2013,7 @@ dependencies = [
[[package]]
name = "leptos_router_macro"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"leptos_macro",
"leptos_router",
@@ -2025,7 +2025,7 @@ dependencies = [
[[package]]
name = "leptos_server"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"any_spawner",
"base64",
@@ -2759,7 +2759,7 @@ dependencies = [
[[package]]
name = "reactive_graph"
version = "0.2.0-rc3"
version = "0.2.2"
dependencies = [
"any_spawner",
"async-lock",
@@ -2782,7 +2782,7 @@ dependencies = [
[[package]]
name = "reactive_stores"
version = "0.2.0-rc3"
version = "0.2.2"
dependencies = [
"any_spawner",
"dashmap",
@@ -2801,7 +2801,7 @@ dependencies = [
[[package]]
name = "reactive_stores_macro"
version = "0.2.0-rc3"
version = "0.2.2"
dependencies = [
"convert_case 0.8.0",
"proc-macro-error2",
@@ -3298,7 +3298,7 @@ dependencies = [
[[package]]
name = "server_fn"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"actix-web",
"actix-ws",
@@ -3323,6 +3323,7 @@ dependencies = [
"reqwest",
"rkyv",
"rmp-serde",
"rustc_version",
"rustversion",
"send_wrapper",
"serde",
@@ -3361,7 +3362,7 @@ dependencies = [
[[package]]
name = "server_fn_macro"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"const_format",
"convert_case 0.8.0",
@@ -3374,9 +3375,9 @@ dependencies = [
[[package]]
name = "server_fn_macro_default"
version = "0.8.0-rc3"
version = "0.8.2"
dependencies = [
"server_fn_macro 0.8.0-rc3",
"server_fn_macro 0.8.2",
"syn 2.0.100",
]
@@ -3570,7 +3571,7 @@ dependencies = [
[[package]]
name = "tachys"
version = "0.2.0-rc3"
version = "0.2.2"
dependencies = [
"any_spawner",
"async-trait",

View File

@@ -40,40 +40,40 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.0-rc3"
version = "0.8.2"
edition = "2021"
rust-version = "1.76"
[workspace.dependencies]
convert_case = "0.8"
throw_error = { path = "./any_error/", version = "0.3.0" }
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc3" }
any_spawner = { path = "./any_spawner/", version = "0.3.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.5" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
itertools = "0.14.0"
leptos = { path = "./leptos", version = "0.8.0-rc3" }
leptos_config = { path = "./leptos_config", version = "0.8.0-rc3" }
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc3" }
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc3" }
leptos_router = { path = "./router", version = "0.8.0-rc3" }
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc3" }
leptos_server = { path = "./leptos_server", version = "0.8.0-rc3" }
leptos_meta = { path = "./meta", version = "0.8.0-rc3" }
leptos = { path = "./leptos", version = "0.8.2" }
leptos_config = { path = "./leptos_config", version = "0.8.2" }
leptos_dom = { path = "./leptos_dom", version = "0.8.2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.2" }
leptos_macro = { path = "./leptos_macro", version = "0.8.2" }
leptos_router = { path = "./router", version = "0.8.2" }
leptos_router_macro = { path = "./router_macro", version = "0.8.2" }
leptos_server = { path = "./leptos_server", version = "0.8.2" }
leptos_meta = { path = "./meta", version = "0.8.2" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.0-rc3" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc3" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc3" }
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.0-rc3" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc3" }
tachys = { path = "./tachys", version = "0.2.0-rc3" }
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"

View File

@@ -1,6 +1,6 @@
[package]
name = "any_spawner"
version = "0.3.0-rc3"
version = "0.3.0"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -26,12 +26,17 @@ pub async fn file_and_error_handler(
.map(|h| h.to_str().unwrap_or("none"))
.unwrap_or("none")
.to_string();
let res = get_static_file(uri.clone(), accept_encoding).await.unwrap();
let static_result = get_static_file(uri.clone(), accept_encoding).await;
if res.status() == StatusCode::OK {
res.into_response()
} else {
(StatusCode::NOT_FOUND, "Not found.").into_response()
match static_result {
Ok(res) => {
if res.status() == StatusCode::OK {
res.into_response()
} else {
(StatusCode::NOT_FOUND, "Not found.").into_response()
}
}
Err(e) => e.into_response(),
}
}

View File

@@ -47,7 +47,7 @@ pub fn Stories() -> impl IntoView {
let stories = Resource::new(
move || (page(), story_type()),
move |(page, story_type)| async move {
fetch_stories(category(&story_type), page).await.ok()
fetch_stories(story_type, page).await.ok()
},
);
let (pending, set_pending) = signal(false);

View File

@@ -13,7 +13,7 @@ pub async fn fetch_story(
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -7,7 +7,7 @@ use send_wrapper::SendWrapper;
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| {
SendWrapper::new(async move {

View File

@@ -10,7 +10,11 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
leptos = { path = "../../leptos", features = [
"tracing",
"islands",
"islands-router",
] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [

View File

@@ -1,6 +1,6 @@
# Leptos Counter Example
# Stores Example
This example creates a simple counter in a client side rendered app with Rust and WASM!
This example shows how to use reactive stores, by building a client-side rendered TODO application.
## Getting Started

View File

@@ -10,19 +10,9 @@ pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
Ok(())
}
pub async fn add_text(client: &Client, text: &str) -> Result<String> {
fill_input(client, text).await?;
get_label(client).await
}
pub async fn fill_input(client: &Client, text: &str) -> Result<()> {
let textbox = find::input(client).await;
textbox.send_keys(text).await?;
Ok(())
}
pub async fn get_label(client: &Client) -> Result<String> {
let label = find::label(client).await;
Ok(label.text().await?)
}

View File

@@ -9,13 +9,3 @@ pub async fn input(client: &Client) -> Element {
textbox
}
pub async fn label(client: &Client) -> Element {
let label = client
.wait()
.for_element(Locator::Css("p"))
.await
.expect("");
label
}

View File

@@ -14,7 +14,7 @@ async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
#[given(regex = "^I add a text as (.*)$")]
async fn i_add_a_text(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::add_text(client, text.as_str()).await?;
action::fill_input(client, text.as_str()).await?;
Ok(())
}

View File

@@ -20,7 +20,7 @@ async fn i_see_the_label_of_the_input_is(
world: &mut AppWorld,
text: String,
) -> Result<()> {
sleep(Duration::from_millis(50)).await;
sleep(Duration::from_millis(500)).await;
let client = &world.client;
check::text_on_element(client, "p", &text).await?;

View File

@@ -4,7 +4,7 @@ authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
version = "0.8.0-rc3"
version = "0.8.2"
rust-version.workspace = true
edition.workspace = true

View File

@@ -99,6 +99,7 @@ trace-component-props = [
"leptos_dom/trace-component-props",
]
delegation = ["tachys/delegation"]
islands-router = ["tachys/mark_branches"]
[build-dependencies]
rustc_version = "0.4.1"

View File

@@ -1,5 +1,5 @@
((root, pkg_path, output_name, wasm_output_name) => {
let MOST_RECENT_CHILDREN_CB;
let MOST_RECENT_CHILDREN_CB = [];
function idle(c) {
if ("requestIdleCallback" in window) {
@@ -22,12 +22,18 @@
traverse(child, children);
}
} else {
if(tag === 'leptos-children') {
MOST_RECENT_CHILDREN_CB = node.$$on_hydrate;
if (tag === 'leptos-children') {
MOST_RECENT_CHILDREN_CB.push(node.$$on_hydrate);
for(const child of node.children) {
traverse(child);
};
// un-set the "most recent children"
MOST_RECENT_CHILDREN_CB.pop();
} else {
for(const child of node.children) {
traverse(child);
};
}
for(const child of node.children) {
traverse(child);
};
}
}
}
@@ -37,8 +43,9 @@
function hydrateIsland(el, id, mod) {
const islandFn = mod[id];
if (islandFn) {
if (MOST_RECENT_CHILDREN_CB) {
MOST_RECENT_CHILDREN_CB();
const children_cb = MOST_RECENT_CHILDREN_CB[MOST_RECENT_CHILDREN_CB.length-1];
if (children_cb) {
children_cb();
}
islandFn(el);
} else {

View File

@@ -17,6 +17,10 @@ window.addEventListener("popstate", async (ev) => {
});
window.addEventListener("submit", async (ev) => {
if (ev.defaultPrevented) {
return;
}
const req = submitToReq(ev);
if(!req) {
return;
@@ -194,6 +198,15 @@ function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) {
else if (oldNode.nodeType === Node.TEXT_NODE) {
oldNode.textContent = newNode.textContent;
}
// islands should not be diffed on the client, because we do not want to overwrite client-side state
// but their children should be diffed still, because they could contain new server content
else if (oldNode.nodeType === Node.ELEMENT_NODE && oldNode.tagName === "LEPTOS-ISLAND") {
// TODO: diff the leptos-children
// skip over leptos-island otherwise
oldDocWalker.nextSibling();
newDocWalker.nextSibling();
}
// if it's an element, replace if it's a different tag, or update attributes
else if (oldNode.nodeType === Node.ELEMENT_NODE) {
diffElement(oldNode, newNode);

View File

@@ -107,21 +107,19 @@ pub fn HydrationScripts(
.unwrap_or_default();
let root = root.unwrap_or_default();
use_context::<IslandsRouterNavigation>().is_none().then(|| {
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
</script>
}
})
view! {
<link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") nonce=nonce.clone()/>
<link
rel="preload"
href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
r#as="fetch"
r#type="application/wasm"
crossorigin=nonce.clone().unwrap_or_default()
/>
<script type="module" nonce=nonce>
{format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
</script>
}
}
/// If this is provided via context, it means that you are using the islands router and

View File

@@ -7,7 +7,7 @@ ws.onmessage = (ev) => {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
let newHref = '/' + msg.css + '?version=' + Date.now();
link.setAttribute('href', newHref);
found = true;
}

View File

@@ -359,23 +359,11 @@ impl ToTokens for Model {
let component = if is_island {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::context::use_context::<::leptos::reactive::owner::IsHydrating>()
.map(|h| h.0)
.unwrap_or(false) {
::leptos::either::Either::Left(
#component
)
} else {
::leptos::either::Either::Right(
::leptos::tachys::html::islands::Island::new(
stringify!(#hydrate_fn_name),
#component
)
#island_serialized_props
)
}
}
::leptos::tachys::html::islands::Island::new(
stringify!(#hydrate_fn_name),
#component
)
#island_serialized_props
}
} else {
component

View File

@@ -300,6 +300,34 @@ impl<T> LocalResource<T> {
pub fn refetch(&self) {
self.refetch.try_update(|n| *n += 1);
}
/// Synchronously, reactively reads the current value of the resource and applies the function
/// `f` to its value if it is `Some(_)`.
#[track_caller]
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
where
T: 'static,
{
self.data.try_with(|n| n.as_ref().map(f))?
}
}
impl<T, E> LocalResource<Result<T, E>>
where
T: 'static,
E: Clone + 'static,
{
/// Applies the given function when a resource that returns `Result<T, E>`
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
/// calls over the `Option<Result<_, _>>` returned by the resource.
///
/// This is useful when used with features like server functions, in conjunction
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
/// left to handle the `None` and `Err(_)` states.
#[track_caller]
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
}
}
impl<T> IntoFuture for LocalResource<T>

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.8.0-rc3"
version = "0.8.2"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -242,23 +242,22 @@ impl ServerMetaContextOutput {
let head_loc = first_chunk
.find("</head>")
.expect("you are using leptos_meta without a </head> tag");
let marker_loc =
first_chunk.find("<!--HEAD-->").unwrap_or_else(|| {
let marker_loc = first_chunk
.find("<!--HEAD-->")
.map(|pos| pos + "<!--HEAD-->".len())
.unwrap_or_else(|| {
first_chunk.find("</head>").unwrap_or(head_loc)
});
let (before_marker, after_marker) =
first_chunk.split_at_mut(marker_loc);
let (before_head_close, after_head) =
after_marker.split_at_mut(head_loc - marker_loc);
buf.push_str(before_marker);
buf.push_str(&meta_buf);
if let Some(title) = title {
buf.push_str("<title>");
buf.push_str(&title);
buf.push_str("</title>");
}
buf.push_str(before_head_close);
buf.push_str(&meta_buf);
buf.push_str(after_head);
buf.push_str(after_marker);
buf
};

View File

@@ -7,6 +7,9 @@ use leptos::{
/// Injects an [`HTMLLinkElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
/// head that loads a stylesheet from the URL given by the `href` property.
///
/// Note that this does *not* work with the `cargo-leptos` `hash-files` feature: if you are using file
/// hashing, you should use [`HashedStylesheet`](crate::HashedStylesheet).
///
/// ```
/// use leptos::prelude::*;
/// use leptos_meta::*;

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.0-rc3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -236,11 +236,11 @@ pub fn provide_context<T: Send + Sync + 'static>(value: T) {
///
/// Effect::new(move |_| {
/// // each use_context clones the value
/// let value =
/// use_context::<String>().expect("could not find i32 in context");
/// let value = use_context::<String>()
/// .expect("could not find String in context");
/// assert_eq!(value, "foo");
/// let value2 =
/// use_context::<String>().expect("could not find i32 in context");
/// let value2 = use_context::<String>()
/// .expect("could not find String in context");
/// assert_eq!(value2, "foo");
/// });
/// });
@@ -284,11 +284,11 @@ pub fn use_context<T: Clone + 'static>() -> Option<T> {
///
/// Effect::new(move |_| {
/// // each use_context clones the value
/// let value =
/// use_context::<String>().expect("could not find i32 in context");
/// let value = use_context::<String>()
/// .expect("could not find String in context");
/// assert_eq!(value, "foo");
/// let value2 =
/// use_context::<String>().expect("could not find i32 in context");
/// let value2 = use_context::<String>()
/// .expect("could not find String in context");
/// assert_eq!(value2, "foo");
/// });
/// });

View File

@@ -26,7 +26,7 @@ use std::{
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
/// without exposing the original type directly to users.
pub struct ArcMappedSignal<T> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
#[allow(clippy::type_complexity)]
try_read_untracked: Arc<
@@ -44,7 +44,7 @@ pub struct ArcMappedSignal<T> {
impl<T> Clone for ArcMappedSignal<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
try_read_untracked: self.try_read_untracked.clone(),
try_write: self.try_write.clone(),
@@ -67,7 +67,7 @@ impl<T> ArcMappedSignal<T> {
U: Send + Sync + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
try_read_untracked: {
let this = inner.clone();
@@ -110,7 +110,7 @@ impl<T> ArcMappedSignal<T> {
impl<T> Debug for ArcMappedSignal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut partial = f.debug_struct("ArcMappedSignal");
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
partial.field("defined_at", &self.defined_at);
partial.finish()
}
@@ -118,11 +118,11 @@ impl<T> Debug for ArcMappedSignal<T> {
impl<T> DefinedAt for ArcMappedSignal<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -228,7 +228,7 @@ where
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
/// without exposing the original type directly to users.
pub struct MappedSignal<T, S = SyncStorage> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: StoredValue<ArcMappedSignal<T>, S>,
}
@@ -246,7 +246,7 @@ impl<T> MappedSignal<T> {
U: Send + Sync + 'static,
{
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: {
let this = ArcRwSignal::from(inner);
@@ -269,7 +269,7 @@ impl<T> Clone for MappedSignal<T> {
impl<T> Debug for MappedSignal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut partial = f.debug_struct("MappedSignal");
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
partial.field("defined_at", &self.defined_at);
partial.finish()
}
@@ -277,11 +277,11 @@ impl<T> Debug for MappedSignal<T> {
impl<T> DefinedAt for MappedSignal<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
@@ -352,7 +352,7 @@ where
#[track_caller]
fn from(value: ArcMappedSignal<T>) -> Self {
MappedSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: StoredValue::new(value),
}

View File

@@ -118,7 +118,7 @@ where
#[track_caller]
fn from(value: ArcWriteSignal<T>) -> Self {
WriteSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}
@@ -132,7 +132,7 @@ where
#[track_caller]
fn from_local(value: ArcWriteSignal<T>) -> Self {
WriteSignal {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(value),
}

View File

@@ -11,7 +11,8 @@ pub mod read {
},
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
ArcMappedSignal, ArcReadSignal, ArcRwSignal, MappedSignal,
ReadSignal, RwSignal,
},
traits::{
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
@@ -807,6 +808,16 @@ pub mod read {
}
}
impl<T> From<MappedSignal<T>> for Signal<T>
where
T: Clone + Send + Sync + 'static,
{
#[track_caller]
fn from(value: MappedSignal<T>) -> Self {
Self::derive(move || value.get())
}
}
impl<T> From<RwSignal<T, LocalStorage>> for Signal<T, LocalStorage>
where
T: 'static,
@@ -839,6 +850,16 @@ pub mod read {
}
}
impl<T> From<ArcMappedSignal<T>> for Signal<T>
where
T: Clone + Send + Sync + 'static,
{
#[track_caller]
fn from(value: ArcMappedSignal<T>) -> Self {
MappedSignal::from(value).into()
}
}
impl<T> From<ArcRwSignal<T>> for Signal<T, LocalStorage>
where
T: Send + Sync + 'static,

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.2.0-rc3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -39,7 +39,7 @@ where
#[track_caller]
fn deref_field(self) -> DerefedField<Self> {
DerefedField {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: self,
}
@@ -51,7 +51,7 @@ where
#[derive(Debug, Copy, Clone)]
pub struct DerefedField<S> {
inner: S,
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
}
@@ -92,11 +92,11 @@ where
<S::Value as Deref>::Target: Sized + 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}

View File

@@ -56,12 +56,12 @@
//!
//! # if false { // don't run effect in doctests
//! Effect::new(move |_| {
//! // you can access individual store withs field a getter
//! // you can access individual store fields with a getter
//! println!("todos: {:?}", &*store.todos().read());
//! });
//! # }
//!
//! // won't notify the effect that listen to `todos`
//! // won't notify the effect that listens to `todos`
//! store.todos().write().push(Todo {
//! label: "Test".to_string(),
//! completed: false,
@@ -69,7 +69,7 @@
//! ```
//! ### Generated traits
//! The [`Store`](macro@Store) macro generates traits for each `struct` to which it is applied. When working
//! within a single file more module, this is not an issue. However, when working with multiple modules
//! within a single file or module, this is not an issue. However, when working with multiple modules
//! or files, one needs to `use` the generated traits. The general pattern is that for each `struct`
//! named `Foo`, the macro generates a trait named `FooStoreFields`. For example:
//! ```rust
@@ -608,6 +608,14 @@ where
}
}
impl<T, S> PartialEq for Store<T, S> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<T, S> Eq for Store<T, S> {}
impl<T> Store<T, LocalStorage>
where
T: 'static,
@@ -759,7 +767,7 @@ where
{
fn from(value: ArcStore<T>) -> Self {
Self {
#[cfg(debug_assertions)]
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: value.defined_at,
inner: ArenaItem::new_with_storage(value),
}

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores_macro"
version = "0.2.0-rc3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.8.0-rc3"
version = "0.8.2"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -418,7 +418,7 @@ impl RenderHtml for MatchedRoute {
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
if mark_branches {
if mark_branches && escape {
buf.open_branch(&self.0);
}
self.1.to_html_with_buf(
@@ -428,8 +428,11 @@ impl RenderHtml for MatchedRoute {
mark_branches,
extra_attrs,
);
if mark_branches {
if mark_branches && escape {
buf.close_branch(&self.0);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
@@ -443,7 +446,7 @@ impl RenderHtml for MatchedRoute {
) where
Self: Sized,
{
if mark_branches {
if mark_branches && escape {
buf.open_branch(&self.0);
}
self.1.to_html_async_with_buf::<OUT_OF_ORDER>(
@@ -453,8 +456,11 @@ impl RenderHtml for MatchedRoute {
mark_branches,
extra_attrs,
);
if mark_branches {
if mark_branches && escape {
buf.close_branch(&self.0);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router_macro"
version = "0.8.0-rc3"
version = "0.8.2"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -16,7 +16,7 @@ server_fn_macro_default = { workspace = true }
const_format = "0.2.33"
const-str = "0.6.2"
xxhash-rust = { version = "0.8.12", features = ["const_xxh64"] }
rustversion = { workspace = true}
rustversion = { workspace = true }
# used across multiple features
serde = { version = "1.0", features = ["derive"] }
send_wrapper = { version = "0.6.0", features = ["futures"], optional = true }
@@ -82,6 +82,9 @@ url = "2"
pin-project-lite = "0.2.15"
tokio = { version = "1.43.0", features = ["rt"], optional = true }
[build-dependencies]
rustc_version = "0.4.1"
[dev-dependencies]
trybuild = { workspace = true }
@@ -238,4 +241,7 @@ skip_feature_sets = [
max_combination_size = 2
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(leptos_debuginfo)'] }
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(leptos_debuginfo)',
'cfg(rustc_nightly)',
] }

8
server_fn/build.rs Normal file
View File

@@ -0,0 +1,8 @@
use rustc_version::{version_meta, Channel};
fn main() {
// Set cfg flags depending on release channel
if matches!(version_meta().unwrap().channel, Channel::Nightly) {
println!("cargo:rustc-cfg=rustc_nightly");
}
}

View File

@@ -16,11 +16,11 @@ error[E0271]: expected `impl Future<Output = ()>` to be a future that resolves t
|
= note: expected enum `Result<_, _>`
found unit type `()`
note: required by a bound in `ServerFn::{synthetic#0}`
note: required by a bound in `ServerFn::{anon_assoc#0}`
--> src/lib.rs
|
| ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{synthetic#0}`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{anon_assoc#0}`
error[E0277]: () is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
--> tests/invalid/empty_return.rs:3:1

View File

@@ -16,11 +16,11 @@ error[E0271]: expected `impl Future<Output = CustomError>` to be a future that r
|
= note: expected enum `Result<_, _>`
found enum `CustomError`
note: required by a bound in `ServerFn::{synthetic#0}`
note: required by a bound in `ServerFn::{anon_assoc#0}`
--> src/lib.rs
|
| ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{synthetic#0}`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `ServerFn::{anon_assoc#0}`
error[E0277]: CustomError is not a `Result` or aliased `Result`. Server functions must return a `Result` or aliased `Result`.
--> tests/invalid/not_result.rs:25:1

View File

@@ -3,6 +3,7 @@
// multiple combinations of features are tested. This ensures this file is only
// run when **only** the browser feature is enabled.
#![cfg(all(
rustc_nightly,
feature = "browser",
not(any(
feature = "postcard",

View File

@@ -1,6 +1,6 @@
[package]
name = "tachys"
version = "0.2.0-rc3"
version = "0.2.2"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -186,6 +186,7 @@ reactive_graph = ["dep:reactive_graph", "dep:any_spawner"]
reactive_stores = ["reactive_graph", "dep:reactive_stores"]
sledgehammer = ["dep:sledgehammer_bindgen", "dep:sledgehammer_utils"]
tracing = ["dep:tracing"]
mark_branches = []
[package.metadata.cargo-all-features]
denylist = ["tracing", "sledgehammer"]

View File

@@ -8,6 +8,7 @@ use crate::{
/// An island of interactivity in an otherwise-inert HTML document.
pub struct Island<View> {
has_element_representation: bool,
component: &'static str,
props_json: String,
view: View,
@@ -19,6 +20,8 @@ impl<View> Island<View> {
/// Creates a new island with the given component name.
pub fn new(component: &'static str, view: View) -> Self {
Island {
has_element_representation:
Self::should_have_element_representation(),
component,
props_json: String::new(),
view,
@@ -51,6 +54,21 @@ impl<View> Island<View> {
buf.push_str(ISLAND_TAG);
buf.push('>');
}
/// Whether this island should be represented by an actual HTML element
fn should_have_element_representation() -> bool {
#[cfg(feature = "reactive_graph")]
{
use reactive_graph::owner::{use_context, IsHydrating};
let already_hydrating =
use_context::<IsHydrating>().map(|h| h.0).unwrap_or(false);
!already_hydrating
}
#[cfg(not(feature = "reactive_graph"))]
{
true
}
}
}
impl<View> Render for Island<View>
@@ -83,11 +101,13 @@ where
Self::Output<NewAttr>: RenderHtml,
{
let Island {
has_element_representation,
component,
props_json,
view,
} = self;
Island {
has_element_representation,
component,
props_json,
view: view.add_any_attr(attr),
@@ -114,11 +134,13 @@ where
async fn resolve(self) -> Self::AsyncOutput {
let Island {
has_element_representation,
component,
props_json,
view,
} = self;
Island {
has_element_representation,
component,
props_json,
view: view.resolve().await,
@@ -133,7 +155,10 @@ where
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
Self::open_tag(self.component, &self.props_json, buf);
let has_element = self.has_element_representation;
if has_element {
Self::open_tag(self.component, &self.props_json, buf);
}
self.view.to_html_with_buf(
buf,
position,
@@ -141,7 +166,9 @@ where
mark_branches,
extra_attrs,
);
Self::close_tag(buf);
if has_element {
Self::close_tag(buf);
}
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -154,9 +181,12 @@ where
) where
Self: Sized,
{
let has_element = self.has_element_representation;
// insert the opening tag synchronously
let mut tag = String::new();
Self::open_tag(self.component, &self.props_json, &mut tag);
if has_element {
Self::open_tag(self.component, &self.props_json, &mut tag);
}
buf.push_sync(&tag);
// streaming render for the view
@@ -170,7 +200,9 @@ where
// and insert the closing tag synchronously
tag.clear();
Self::close_tag(&mut tag);
if has_element {
Self::close_tag(&mut tag);
}
buf.push_sync(&tag);
}
@@ -179,18 +211,21 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if position.get() == Position::FirstChild {
cursor.child();
} else if position.get() == Position::NextChild {
cursor.sibling();
if self.has_element_representation {
if position.get() == Position::FirstChild {
cursor.child();
} else if position.get() == Position::NextChild {
cursor.sibling();
}
position.set(Position::FirstChild);
}
position.set(Position::FirstChild);
self.view.hydrate::<FROM_SERVER>(cursor, position)
}
fn into_owned(self) -> Self::Owned {
Island {
has_element_representation: self.has_element_representation,
component: self.component,
props_json: self.props_json,
view: self.view.into_owned(),
@@ -358,6 +393,7 @@ where
} else if curr_position != Position::Current {
cursor.sibling();
}
position.set(Position::NextChild);
if let Some(on_hydrate) = self.on_hydrate {
use crate::{

View File

@@ -84,22 +84,27 @@ where
*self.0.borrow_mut() = node;
}
/// Advances to the next placeholder node.
/// Advances to the next placeholder node and returns it
pub fn next_placeholder(
&self,
position: &PositionState,
) -> crate::renderer::types::Placeholder {
//crate::dom::log("looking for placeholder after");
//Rndr::log_node(&self.current());
self.advance_to_placeholder(position);
let marker = self.current();
crate::renderer::types::Placeholder::cast_from(marker.clone())
.unwrap_or_else(|| failed_to_cast_marker_node(marker))
}
/// Advances to the next placeholder node.
pub fn advance_to_placeholder(&self, position: &PositionState) {
if position.get() == Position::FirstChild {
self.child();
} else {
self.sibling();
}
let marker = self.current();
position.set(Position::NextChild);
crate::renderer::types::Placeholder::cast_from(marker.clone())
.unwrap_or_else(|| failed_to_cast_marker_node(marker))
}
}

View File

@@ -285,6 +285,15 @@ where
}
}
impl<T> Drop for RenderEffectState<T> {
fn drop(&mut self) {
if let Some(effect) = self.0.take() {
drop(effect.take_value());
drop(effect);
}
}
}
impl<M, E> Mountable for Result<M, E>
where
M: Mountable,

View File

@@ -223,10 +223,20 @@ impl StreamBuilder {
);
}
let chunks = subbuilder.finish().take_chunks();
let mut flattened_chunks =
VecDeque::with_capacity(chunks.len());
for chunk in chunks {
// this will wait for any ErrorBoundary async nodes and flatten them out
if let StreamChunk::Async { chunks } = chunk {
flattened_chunks.extend(chunks.await);
} else {
flattened_chunks.push_back(chunk);
}
}
OooChunk {
id,
chunks,
chunks: flattened_chunks,
replace,
nonce,
}

View File

@@ -392,10 +392,12 @@ impl RenderHtml for AnyView {
) {
#[cfg(feature = "ssr")]
{
let type_id = mark_branches
.then(|| format!("{:?}", self.type_id))
.unwrap_or_default();
if mark_branches {
let type_id = if mark_branches && escape {
format!("{:?}", self.type_id)
} else {
Default::default()
};
if mark_branches && escape {
buf.open_branch(&type_id);
}
(self.to_html)(
@@ -406,8 +408,11 @@ impl RenderHtml for AnyView {
mark_branches,
extra_attrs,
);
if mark_branches {
if mark_branches && escape {
buf.close_branch(&type_id);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
#[cfg(not(feature = "ssr"))]
@@ -436,6 +441,14 @@ impl RenderHtml for AnyView {
{
#[cfg(feature = "ssr")]
if OUT_OF_ORDER {
let type_id = if mark_branches && escape {
format!("{:?}", self.type_id)
} else {
Default::default()
};
if mark_branches && escape {
buf.open_branch(&type_id);
}
(self.to_html_async_ooo)(
self.value,
buf,
@@ -444,11 +457,19 @@ impl RenderHtml for AnyView {
mark_branches,
extra_attrs,
);
if mark_branches && escape {
buf.close_branch(&type_id);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
} else {
let type_id = mark_branches
.then(|| format!("{:?}", self.type_id))
.unwrap_or_default();
if mark_branches {
let type_id = if mark_branches && escape {
format!("{:?}", self.type_id)
} else {
Default::default()
};
if mark_branches && escape {
buf.open_branch(&type_id);
}
(self.to_html_async)(
@@ -459,8 +480,11 @@ impl RenderHtml for AnyView {
mark_branches,
extra_attrs,
);
if mark_branches {
if mark_branches && escape {
buf.close_branch(&type_id);
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
#[cfg(not(feature = "ssr"))]
@@ -483,13 +507,23 @@ impl RenderHtml for AnyView {
position: &PositionState,
) -> Self::State {
#[cfg(feature = "hydrate")]
if FROM_SERVER {
(self.hydrate_from_server)(self.value, cursor, position)
} else {
panic!(
"hydrating AnyView from inside a ViewTemplate is not \
supported."
);
{
if FROM_SERVER {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state =
(self.hydrate_from_server)(self.value, cursor, position);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
} else {
panic!(
"hydrating AnyView from inside a ViewTemplate is not \
supported."
);
}
}
#[cfg(not(feature = "hydrate"))]
{

View File

@@ -308,7 +308,7 @@ where
) {
match self {
Either::Left(left) => {
if mark_branches {
if mark_branches && escape {
buf.open_branch("0");
}
left.to_html_with_buf(
@@ -318,12 +318,15 @@ where
mark_branches,
extra_attrs,
);
if mark_branches {
if mark_branches && escape {
buf.close_branch("0");
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
Either::Right(right) => {
if mark_branches {
if mark_branches && escape {
buf.open_branch("1");
}
right.to_html_with_buf(
@@ -333,8 +336,11 @@ where
mark_branches,
extra_attrs,
);
if mark_branches {
if mark_branches && escape {
buf.close_branch("1");
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
}
@@ -352,7 +358,7 @@ where
{
match self {
Either::Left(left) => {
if mark_branches {
if mark_branches && escape {
buf.open_branch("0");
}
left.to_html_async_with_buf::<OUT_OF_ORDER>(
@@ -362,12 +368,15 @@ where
mark_branches,
extra_attrs,
);
if mark_branches {
if mark_branches && escape {
buf.close_branch("0");
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
Either::Right(right) => {
if mark_branches {
if mark_branches && escape {
buf.open_branch("1");
}
right.to_html_async_with_buf::<OUT_OF_ORDER>(
@@ -377,8 +386,11 @@ where
mark_branches,
extra_attrs,
);
if mark_branches {
if mark_branches && escape {
buf.close_branch("1");
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
}
}
@@ -389,14 +401,21 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
match self {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
Either::Left(left) => {
Either::Left(left.hydrate::<FROM_SERVER>(cursor, position))
}
Either::Right(right) => {
Either::Right(right.hydrate::<FROM_SERVER>(cursor, position))
}
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
state
}
fn into_owned(self) -> Self::Owned {
@@ -849,12 +868,15 @@ macro_rules! tuples {
) {
match self {
$([<EitherOf $num>]::$ty(this) => {
if mark_branches {
if mark_branches && escape {
buf.open_branch(stringify!($ty));
}
this.to_html_with_buf(buf, position, escape, mark_branches, extra_attrs);
if mark_branches {
if mark_branches && escape {
buf.close_branch(stringify!($ty));
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
})*
}
@@ -872,12 +894,15 @@ macro_rules! tuples {
{
match self {
$([<EitherOf $num>]::$ty(this) => {
if mark_branches {
if mark_branches && escape {
buf.open_branch(stringify!($ty));
}
this.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position, escape, mark_branches, extra_attrs);
if mark_branches {
if mark_branches && escape {
buf.close_branch(stringify!($ty));
if *position == Position::NextChildAfterText {
*position = Position::NextChild;
}
}
})*
}
@@ -888,11 +913,17 @@ macro_rules! tuples {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let state = match self {
$([<EitherOf $num>]::$ty(this) => {
[<EitherOf $num>]::$ty(this.hydrate::<FROM_SERVER>(cursor, position))
})*
};
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
Self::State { state }
}

View File

@@ -33,11 +33,10 @@ where
match (&mut state.state, self) {
// both errors: throw the new error and replace
(Either::Right(_), Err(new)) => {
if let Some(old_error) =
state.error.replace(throw_error::throw(new.into()))
{
if let Some(old_error) = state.error.take() {
throw_error::clear(&old_error);
}
state.error = Some(throw_error::throw(new.into()));
}
// both Ok: need to rebuild child
(Either::Left(old), Ok(new)) => {

View File

@@ -435,7 +435,7 @@ where
T: Mountable,
{
states: Vec<T>,
parent: Option<crate::renderer::types::Element>,
marker: crate::renderer::types::Placeholder,
}
impl<T> Mountable for StaticVecState<T>
@@ -443,7 +443,10 @@ where
T: Mountable,
{
fn unmount(&mut self) {
self.states.iter_mut().for_each(Mountable::unmount);
for state in self.states.iter_mut() {
state.unmount();
}
self.marker.unmount();
}
fn mount(
@@ -454,7 +457,7 @@ where
for state in self.states.iter_mut() {
state.mount(parent, marker);
}
self.parent = Some(parent.clone());
self.marker.mount(parent, marker);
}
fn insert_before_this(&self, child: &mut dyn Mountable) -> bool {
@@ -463,7 +466,7 @@ where
return true;
}
}
false
self.marker.insert_before_this(child)
}
fn elements(&self) -> Vec<crate::renderer::types::Element> {
@@ -481,27 +484,52 @@ where
type State = StaticVecState<T::State>;
fn build(self) -> Self::State {
let marker = Rndr::create_placeholder();
Self::State {
states: self.0.into_iter().map(T::build).collect(),
parent: None,
marker,
}
}
fn rebuild(self, state: &mut Self::State) {
let Self::State { states, .. } = state;
let StaticVecState { states, marker } = state;
let old = states;
// StaticVec's in general shouldn't need to be reused, but rebuild() will still trigger e.g. if 2 routes have the same tree,
// this can cause problems if differing in lengths. Because we don't use marker nodes in StaticVec, we rebuild the entire vec remounting to the parent.
for state in states {
state.unmount();
// reuses the Vec impl
if old.is_empty() {
let mut new = self.build().states;
for item in new.iter_mut() {
Rndr::mount_before(item, marker.as_ref());
}
*old = new;
} else if self.0.is_empty() {
// TODO fast path for clearing
for item in old.iter_mut() {
item.unmount();
}
old.clear();
} else {
let mut adds = vec![];
let mut removes_at_end = 0;
for item in self.0.into_iter().zip_longest(old.iter_mut()) {
match item {
itertools::EitherOrBoth::Both(new, old) => {
T::rebuild(new, old)
}
itertools::EitherOrBoth::Left(new) => {
let mut new_state = new.build();
Rndr::mount_before(&mut new_state, marker.as_ref());
adds.push(new_state);
}
itertools::EitherOrBoth::Right(old) => {
removes_at_end += 1;
old.unmount()
}
}
}
old.truncate(old.len() - removes_at_end);
old.append(&mut adds);
}
let parent = state
.parent
.take()
.expect("parent should always be Some() on a StaticVec rebuild()");
*state = self.build();
state.mount(&parent, None);
}
}
@@ -552,7 +580,7 @@ where
}
fn html_len(&self) -> usize {
self.0.iter().map(RenderHtml::html_len).sum::<usize>()
self.0.iter().map(RenderHtml::html_len).sum::<usize>() + 3
}
fn to_html_with_buf(
@@ -572,6 +600,10 @@ where
extra_attrs.clone(),
);
}
if escape {
buf.push_str("<!>");
*position = Position::NextChild;
}
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
@@ -593,6 +625,10 @@ where
extra_attrs.clone(),
);
}
if escape {
buf.push_sync("<!>");
*position = Position::NextChild;
}
}
fn hydrate<const FROM_SERVER: bool>(
@@ -605,8 +641,11 @@ where
.into_iter()
.map(|child| child.hydrate::<FROM_SERVER>(cursor, position))
.collect();
let parent = cursor.current().parent_element();
Self::State { states, parent }
let marker = cursor.next_placeholder(position);
position.set(Position::NextChild);
Self::State { states, marker }
}
fn into_owned(self) -> Self::Owned {

View File

@@ -252,12 +252,12 @@ where
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
if mark_branches {
if mark_branches && escape {
buf.open_branch("for");
}
for (index, item) in self.items.into_iter().enumerate() {
let (_, item) = (self.view_fn)(index, item);
if mark_branches {
if mark_branches && escape {
buf.open_branch("item");
}
item.to_html_with_buf(
@@ -267,12 +267,12 @@ where
mark_branches,
extra_attrs.clone(),
);
if mark_branches {
if mark_branches && escape {
buf.close_branch("item");
}
*position = Position::NextChild;
}
if mark_branches {
if mark_branches && escape {
buf.close_branch("for");
}
buf.push_str("<!>");
@@ -286,7 +286,7 @@ where
mark_branches: bool,
extra_attrs: Vec<AnyAttribute>,
) {
if mark_branches {
if mark_branches && escape {
buf.open_branch("for");
}
for (index, item) in self.items.into_iter().enumerate() {
@@ -296,7 +296,7 @@ where
format!("item-{key}")
});
let (_, item) = (self.view_fn)(index, item);
if mark_branches {
if mark_branches && escape {
buf.open_branch(branch_name.as_ref().unwrap());
}
item.to_html_async_with_buf::<OUT_OF_ORDER>(
@@ -306,12 +306,12 @@ where
mark_branches,
extra_attrs.clone(),
);
if mark_branches {
if mark_branches && escape {
buf.close_branch(branch_name.as_ref().unwrap());
}
*position = Position::NextChild;
}
if mark_branches {
if mark_branches && escape {
buf.close_branch("for");
}
buf.push_sync("<!>");
@@ -322,6 +322,10 @@ where
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
// get parent and position
let current = cursor.current();
let parent = if position.get() == Position::FirstChild {
@@ -342,11 +346,22 @@ where
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
let item = view.hydrate::<FROM_SERVER>(cursor, position);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
rendered_items.push(Some((set_index, item)));
}
let marker = cursor.next_placeholder(position);
position.set(Position::NextChild);
if cfg!(feature = "mark_branches") {
cursor.advance_to_placeholder(position);
}
KeyedState {
parent: Some(parent),
marker,