Skip to content

Commit d80a4a8

Browse files
authored
Merge pull request #25 from rameel/fix-x-format
Fix `x-format` directive failing on nested `x-data` elements
2 parents 2454640 + 86c85f4 commit d80a4a8

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

src/plugins/format/index.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ function plugin({ directive, evaluateLater, mutateDom }) {
55
directive("format", (el, { modifiers }, { effect }) => {
66
const placeholder_regex = /{{(?<expr>.+?)}}/g;
77
const is_once = has_modifier(modifiers, "once");
8+
const has_format_attr = el => el.hasAttribute("x-format");
89

910
process(el);
1011

@@ -66,6 +67,29 @@ function plugin({ directive, evaluateLater, mutateDom }) {
6667

6768
function process_nodes(node) {
6869
for (let child of node.childNodes) {
70+
if (child.nodeType === Node.ELEMENT_NODE) {
71+
//
72+
// When we encounter an element with the 'x-data' attribute, its properties
73+
// are not yet initialized, and the Alpine context is unavailable.
74+
// Attempting to use these properties will result in
75+
// an "Alpine Expression Error: [expression] is not defined".
76+
//
77+
// Workaround:
78+
// To avoid this, we manually add our 'x-format' directive to the element.
79+
// Alpine evaluates 'x-format' directive once the context is initialized.
80+
// In the current loop, we skip these elements to defer their processing.
81+
//
82+
// This also handles cases where the user manually adds the 'x-format' attribute.
83+
//
84+
if (child.hasAttribute("x-data") && !has_format_attr(child)) {
85+
child.setAttribute("x-format", "");
86+
}
87+
88+
if (has_format_attr(child)) {
89+
continue;
90+
}
91+
}
92+
6993
process(child);
7094
}
7195
}

tests/playwright/x-format.spec.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,48 @@ test("x-format: context aware", async ({ page }) => {
4141
await expect(page.locator("#id_4")).toContainText("id_4");
4242
await expect(page.locator("#id_3")).toHaveAttribute("title", "id_3");
4343
});
44+
45+
test("x-format: nested x-data", async ({ page }) => {
46+
await set_html(page, `
47+
<div x-data="{ value1: 3.14 }" x-format>
48+
<div x-data="{ value2: 2.71 }">
49+
<span id="v2">{{ value2 }}</span>
50+
<button id="b2" @click="value2 = 'math.e'">Change</button>
51+
</div>
52+
53+
<span id="v1">{{ value1 }}</span>
54+
<button id="b1" @click="value1 = 'math.pi'">Change</button>
55+
</div>`);
56+
57+
await expect(page.locator("#v1")).toContainText("3.14");
58+
await expect(page.locator("#v2")).toContainText("2.71");
59+
60+
await page.locator("#b1").click();
61+
await expect(page.locator("#v1")).toContainText("math.pi");
62+
63+
await page.locator("#b2").click();
64+
await expect(page.locator("#v2")).toContainText("math.e");
65+
});
66+
67+
test("x-format: nested x-data with manually x-format", async ({ page }) => {
68+
await set_html(page, `
69+
<div x-data="{ value1: 3.14 }" x-format>
70+
<div x-data="{ value2: 2.71 }" x-format>
71+
<span id="v2">{{ value2 }}</span>
72+
<button id="b2" @click="value2 = 'math.e'">Change</button>
73+
</div>
74+
75+
<span id="v1">{{ value1 }}</span>
76+
<button id="b1" @click="value1 = 'math.pi'">Change</button>
77+
</div>`);
78+
79+
await expect(page.locator("#v1")).toContainText("3.14");
80+
await expect(page.locator("#v2")).toContainText("2.71");
81+
82+
await page.locator("#b1").click();
83+
await expect(page.locator("#v1")).toContainText("math.pi");
84+
85+
await page.locator("#b2").click();
86+
await expect(page.locator("#v2")).toContainText("math.e");
87+
});
88+

0 commit comments

Comments
 (0)