Skip to content

Commit d1fa20a

Browse files
authored
Fix ignored boolean attributes getting set by the server (#4050)
Closes #4049
1 parent 3df1e6c commit d1fa20a

File tree

3 files changed

+62
-11
lines changed

3 files changed

+62
-11
lines changed

assets/js/phoenix_live_view/dom.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,15 @@ const DOM = {
747747
isLocked(el) {
748748
return el.hasAttribute && el.hasAttribute(PHX_REF_LOCK);
749749
},
750+
751+
attributeIgnored(attribute, ignoredAttributes) {
752+
return ignoredAttributes.some(
753+
(toIgnore) =>
754+
attribute.name == toIgnore ||
755+
toIgnore === "*" ||
756+
(toIgnore.includes("*") && attribute.name.match(toIgnore) != null),
757+
);
758+
},
750759
};
751760

752761
export default DOM;

assets/js/phoenix_live_view/js.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -329,15 +329,19 @@ const JS = {
329329
ignoreAttrs(el, attrs) {
330330
DOM.putPrivate(el, "JS:ignore_attrs", {
331331
apply: (fromEl, toEl) => {
332-
Array.from(fromEl.attributes).forEach((attr) => {
333-
if (
334-
attrs.some(
335-
(toIgnore) =>
336-
attr.name == toIgnore ||
337-
toIgnore === "*" ||
338-
(toIgnore.includes("*") && attr.name.match(toIgnore) != null),
339-
)
340-
) {
332+
let fromAttributes = Array.from(fromEl.attributes);
333+
let fromAttributeNames = fromAttributes.map((attr) => attr.name);
334+
Array.from(toEl.attributes)
335+
.filter((attr) => {
336+
return !fromAttributeNames.includes(attr.name);
337+
})
338+
.forEach((attr) => {
339+
if (DOM.attributeIgnored(attr, attrs)) {
340+
toEl.removeAttribute(attr.name);
341+
}
342+
});
343+
fromAttributes.forEach((attr) => {
344+
if (DOM.attributeIgnored(attr, attrs)) {
341345
toEl.setAttribute(attr.name, attr.value);
342346
}
343347
});

assets/test/view_test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,40 @@ describe("View + DOM", function () {
996996
expect(view.el.firstChild.textContent.replace(/\s+/g, "")).toEqual("A1");
997997
});
998998

999+
test("ignore_attributes skips boolean attributes on update when not set", () => {
1000+
let liveSocket = new LiveSocket("/live", Socket);
1001+
let el = liveViewDOM();
1002+
let updateDiff = {
1003+
"0": ' phx-mounted="[["ignore_attrs",{"attrs":["open"]}]]"',
1004+
"1": "0",
1005+
s: [
1006+
"<details open",
1007+
">\n <summary>A</summary>\n <span>",
1008+
"</span></details>",
1009+
],
1010+
};
1011+
1012+
let view = simulateJoinedView(el, liveSocket);
1013+
view.applyDiff("update", updateDiff, ({ diff, events }) =>
1014+
view.update(diff, events),
1015+
);
1016+
1017+
expect(view.el.firstChild.tagName).toBe("DETAILS");
1018+
expect(view.el.firstChild.open).toBe(true);
1019+
view.el.firstChild.open = false;
1020+
view.el.firstChild.setAttribute("data-foo", "bar");
1021+
1022+
// now update, the HTML patch would normally reset the open attribute
1023+
view.applyDiff("update", { "1": "1" }, ({ diff, events }) =>
1024+
view.update(diff, events),
1025+
);
1026+
// open is ignored, so it is kept as is
1027+
expect(view.el.firstChild.open).toBe(false);
1028+
// foo is not ignored, so it is reset
1029+
expect(view.el.firstChild.getAttribute("data-foo")).toBe(null);
1030+
expect(view.el.firstChild.textContent.replace(/\s+/g, "")).toEqual("A1");
1031+
});
1032+
9991033
test("ignore_attributes wildcard", () => {
10001034
let liveSocket = new LiveSocket("/live", Socket);
10011035
let el = liveViewDOM();
@@ -1031,8 +1065,10 @@ describe("View + DOM", function () {
10311065
expect(view.el.firstChild.getAttribute("data-foo")).toBe("bar");
10321066
expect(view.el.firstChild.getAttribute("data-bar")).toBe("bar");
10331067
expect(view.el.firstChild.getAttribute("data-other")).toBe("also kept");
1034-
expect(view.el.firstChild.getAttribute("data-new")).toBe("new");
10351068
expect(view.el.firstChild.textContent.replace(/\s+/g, "")).toEqual("A1");
1069+
1070+
// Not added for being ignored
1071+
expect(view.el.firstChild.getAttribute("data-new")).toBe(null);
10361072
});
10371073

10381074
test("ignore_attributes *", () => {
@@ -1072,8 +1108,10 @@ describe("View + DOM", function () {
10721108
expect(view.el.firstChild.getAttribute("data-bar")).toBe("bar");
10731109
expect(view.el.firstChild.getAttribute("something")).toBe("else");
10741110
expect(view.el.firstChild.getAttribute("data-other")).toBe("also kept");
1075-
expect(view.el.firstChild.getAttribute("data-new")).toBe("new");
10761111
expect(view.el.firstChild.textContent.replace(/\s+/g, "")).toEqual("A1");
1112+
1113+
// Not added for being ignored
1114+
expect(view.el.firstChild.getAttribute("data-new")).toBe(null);
10771115
});
10781116
});
10791117
});

0 commit comments

Comments
 (0)