diff --git a/Cargo.lock b/Cargo.lock index 67990fbd1..d5441f625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5926,6 +5926,13 @@ dependencies = [ "tokio", ] +[[package]] +name = "dioxus-playwright-fullstack-spread-test" +version = "0.1.0" +dependencies = [ + "dioxus", +] + [[package]] name = "dioxus-playwright-fullstack-test" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 05b610569..040ac9639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,6 +130,7 @@ members = [ "packages/playwright-tests/fullstack", "packages/playwright-tests/fullstack-errors", "packages/playwright-tests/fullstack-mounted", + "packages/playwright-tests/fullstack-spread", "packages/playwright-tests/fullstack-routing", "packages/playwright-tests/fullstack-hydration-order", "packages/playwright-tests/suspense-carousel", diff --git a/packages/core/src/diff/node.rs b/packages/core/src/diff/node.rs index bb1cae203..6c8e7a26e 100644 --- a/packages/core/src/diff/node.rs +++ b/packages/core/src/diff/node.rs @@ -819,10 +819,14 @@ impl VNode { } }; + // Write the value for each attribute in the group for attr in &**attribute { self.write_attribute(attribute_path, attr, id, mount, dom, to); - dom.set_mounted_dyn_attr(mount, attribute_idx, id); } + // Set the mounted dynamic attribute once. This must be set even if no actual + // attributes are present so it is present for renderers like fullstack to look + // up the position where attributes may be inserted in the future + dom.set_mounted_dyn_attr(mount, attribute_idx, id); } } diff --git a/packages/playwright-tests/fullstack-spread.spec.js b/packages/playwright-tests/fullstack-spread.spec.js new file mode 100644 index 000000000..142438b66 --- /dev/null +++ b/packages/playwright-tests/fullstack-spread.spec.js @@ -0,0 +1,13 @@ +// @ts-check +const { test, expect } = require("@playwright/test"); + +test("spread attributes hydrate", async ({ page }) => { + await page.goto("http://localhost:7979"); + + // Expect the page to contain the button + const counter = page.locator("#counter"); + await expect(counter).toHaveText("Count: 0"); + // Clicking on the button should increment the count + await counter.click(); + await expect(counter).toHaveText("Count: 1"); +}); diff --git a/packages/playwright-tests/fullstack-spread/Cargo.toml b/packages/playwright-tests/fullstack-spread/Cargo.toml new file mode 100644 index 000000000..437c91a29 --- /dev/null +++ b/packages/playwright-tests/fullstack-spread/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dioxus-playwright-fullstack-spread-test" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus = { workspace = true, features = ["fullstack"] } + +[features] +default = [] +server = ["dioxus/server"] +web = ["dioxus/web"] diff --git a/packages/playwright-tests/fullstack-spread/src/main.rs b/packages/playwright-tests/fullstack-spread/src/main.rs new file mode 100644 index 000000000..0fe091c84 --- /dev/null +++ b/packages/playwright-tests/fullstack-spread/src/main.rs @@ -0,0 +1,40 @@ +//! Regression test for + +use dioxus::prelude::*; + +fn main() { + dioxus::launch(|| { + rsx! { + Comp {} + Comp {} + Button {} + } + }); +} + +#[component] +fn Button() -> Element { + let mut count = use_signal(|| 0); + + rsx! { + button { + id: "counter", + onclick: move |_| { + count += 1; + }, + "Count: {count}" + } + } +} + +#[component] +fn Comp(#[props(extends = GlobalAttributes)] attributes: Vec) -> Element { + rsx! { + div { + width: 100, + div { + ..attributes, + } + } + } +} diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index 1ef300359..17ebf4c71 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -108,6 +108,15 @@ module.exports = defineConfig({ reuseExistingServer: !process.env.CI, stdout: "pipe", }, + { + cwd: path.join(process.cwd(), "fullstack-spread"), + command: + 'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 7979', + port: 7979, + timeout: 50 * 60 * 1000, + reuseExistingServer: !process.env.CI, + stdout: "pipe", + }, { cwd: path.join(process.cwd(), "fullstack-routing"), command: diff --git a/packages/web/src/hydration/hydrate.rs b/packages/web/src/hydration/hydrate.rs index dcc72b9b1..f06ac1e87 100644 --- a/packages/web/src/hydration/hydrate.rs +++ b/packages/web/src/hydration/hydrate.rs @@ -301,9 +301,12 @@ impl WebsysDom { let id = vnode .mounted_dynamic_attribute(*id, dom) .ok_or(VNodeNotInitialized)?; + // We always need to hydrate the node even if the attributes are empty so we have + // a mount for the node later. This could be spread attributes that are currently empty, + // but will be filled later + mounted_id = Some(id); for attribute in attributes { let value = &attribute.value; - mounted_id = Some(id); if let AttributeValue::Listener(_) = value { if attribute.name == "onmounted" { to_mount.push(id);