Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions src/plugins/bound/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom,
break;

case "open":
process_details();
process_open_attribute();
break;

case "group":
Expand Down Expand Up @@ -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) {
//
// <details>:
// Supports safe two-way binding via the "open" attribute,
// so we initialize from the element only if the bound value
// is null or undefined.
//
// <dialog>:
// 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 "<details>"
//
is_details && effect(update_property);
cleanup(listen(el, "toggle", update_variable));
processed = true;
}
Expand Down
73 changes: 73 additions & 0 deletions tests/playwright/x-bound.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, `
<div x-data="{ open: false }">
<dialog &open open>
Hello World!
</dialog>
<span x-format>{{ open }}</span>
</div>
`);

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, `
<div x-data="{ open: true }">
<dialog &open>
Hello World!
</dialog>
<span x-format>{{ open }}</span>
</div>
`);

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, `
<div x-data="{ open: true }">
<dialog &open>
Hello World!
</dialog>
<button @click="open = true">Toggle</button>
<span x-format>{{ open }}</span>
</div>
`);

// 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, `
<div x-data="{ open: false }">
<dialog &open>
Hello World!
</dialog>
<span x-format>{{ open }}</span>
</div>
`);

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, `
Expand Down