From cb64cf6a8c22f1477d84ccd65271c129a54f75b2 Mon Sep 17 00:00:00 2001 From: rameel Date: Mon, 15 Dec 2025 00:54:45 +0500 Subject: [PATCH] x-bound: add open attribute support --- src/plugins/bound/index.js | 32 ++++++++++---- tests/playwright/x-bound.spec.js | 73 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/plugins/bound/index.js b/src/plugins/bound/index.js index c81b197..12246db 100644 --- a/src/plugins/bound/index.js +++ b/src/plugins/bound/index.js @@ -105,7 +105,7 @@ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom, break; case "open": - process_details(); + process_open_attribute(); break; case "group": @@ -241,13 +241,29 @@ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom, processed = true; } - function process_details() { - if (tag_name === "DETAILS") { - // if the value of the bound property is "null" or "undefined", - // we initialize it with the value from the element. - is_nullish(get_value()) && update_variable(); - - effect(update_property); + function process_open_attribute() { + const [is_details, is_dialog] = [tag_name === "DETAILS", tag_name === "DIALOG"]; + + if (is_details || is_dialog) { + // + //
: + // Supports safe two-way binding via the "open" attribute, + // so we initialize from the element only if the bound value + // is null or undefined. + // + // : + // Directly setting element.open is discouraged by the spec, + // as it breaks native dialog behavior and the "close" event. + // Therefore, we always initialize state from the element + // and treat it as a one-way source of truth. + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/open#value + // + (is_dialog || is_nullish(get_value())) && update_variable(); + + // + // Enable two-way binding only for "
" + // + is_details && effect(update_property); cleanup(listen(el, "toggle", update_variable)); processed = true; } diff --git a/tests/playwright/x-bound.spec.js b/tests/playwright/x-bound.spec.js index 4d00a70..d70ef97 100644 --- a/tests/playwright/x-bound.spec.js +++ b/tests/playwright/x-bound.spec.js @@ -541,6 +541,79 @@ test.describe("x-bound: details", () => { }); }); +test.describe("x-bound: dialog", () => { + test("dialog: initialize state from element", async ({ page }) => { + await set_html(page, ` +
+ + Hello World! + + {{ open }} +
+ `); + + await expect(page.locator("dialog")).toHaveAttribute("open"); + await expect(page.locator("span")).toHaveText("true"); + }); + + test("dialog: always initialize from element", async ({ page }) => { + await set_html(page, ` +
+ + Hello World! + + {{ open }} +
+ `); + + await expect(page.locator("dialog")).not.toHaveAttribute("open"); + await expect(page.locator("span")).toHaveText("false"); + }); + + test("dialog: property changes do not directly control element.open", async ({ page }) => { + await set_html(page, ` +
+ + Hello World! + + + {{ open }} +
+ `); + + // initial state comes from dialog (closed by default) + await expect(page.locator("dialog")).not.toHaveAttribute("open"); + await expect(page.locator("span")).toHaveText("false"); + + await page.locator("button").click(); + + // property changes, but dialog state is not affected + await expect(page.locator("dialog")).not.toHaveAttribute("open"); + await expect(page.locator("span")).toHaveText("true"); + }); + + test("dialog: native open updates bound state", async ({ page }) => { + await set_html(page, ` +
+ + Hello World! + + {{ open }} +
+ `); + + await page.evaluate(() => document.querySelector("dialog").showModal()); + + await expect(page.locator("dialog")).toHaveAttribute("open"); + await expect(page.locator("span")).toHaveText("true"); + + await page.evaluate(() => document.querySelector("dialog").close()); + + await expect(page.locator("dialog")).not.toHaveAttribute("open"); + await expect(page.locator("span")).toHaveText("false"); + }); +}); + test.describe("x-bound: group", () => { test("radio", async ({ page }) => { await set_html(page, `