diff --git a/assets/js/phoenix_live_view/dom.js b/assets/js/phoenix_live_view/dom.js index 2fe01f7dc7..795a6c2ddc 100644 --- a/assets/js/phoenix_live_view/dom.js +++ b/assets/js/phoenix_live_view/dom.js @@ -747,6 +747,15 @@ const DOM = { isLocked(el) { return el.hasAttribute && el.hasAttribute(PHX_REF_LOCK); }, + + attributeIgnored(attribute, ignoredAttributes) { + return ignoredAttributes.some( + (toIgnore) => + attribute.name == toIgnore || + toIgnore === "*" || + (toIgnore.includes("*") && attribute.name.match(toIgnore) != null), + ); + }, }; export default DOM; diff --git a/assets/js/phoenix_live_view/js.js b/assets/js/phoenix_live_view/js.js index a26c097228..09cc48b306 100644 --- a/assets/js/phoenix_live_view/js.js +++ b/assets/js/phoenix_live_view/js.js @@ -329,15 +329,19 @@ const JS = { ignoreAttrs(el, attrs) { DOM.putPrivate(el, "JS:ignore_attrs", { apply: (fromEl, toEl) => { - Array.from(fromEl.attributes).forEach((attr) => { - if ( - attrs.some( - (toIgnore) => - attr.name == toIgnore || - toIgnore === "*" || - (toIgnore.includes("*") && attr.name.match(toIgnore) != null), - ) - ) { + let fromAttributes = Array.from(fromEl.attributes); + let fromAttributeNames = fromAttributes.map((attr) => attr.name); + Array.from(toEl.attributes) + .filter((attr) => { + return !fromAttributeNames.includes(attr.name); + }) + .forEach((attr) => { + if (DOM.attributeIgnored(attr, attrs)) { + toEl.removeAttribute(attr.name); + } + }); + fromAttributes.forEach((attr) => { + if (DOM.attributeIgnored(attr, attrs)) { toEl.setAttribute(attr.name, attr.value); } }); diff --git a/assets/test/view_test.ts b/assets/test/view_test.ts index 504719fc48..58607fed86 100644 --- a/assets/test/view_test.ts +++ b/assets/test/view_test.ts @@ -996,6 +996,40 @@ describe("View + DOM", function () { expect(view.el.firstChild.textContent.replace(/\s+/g, "")).toEqual("A1"); }); + test("ignore_attributes skips boolean attributes on update when not set", () => { + let liveSocket = new LiveSocket("/live", Socket); + let el = liveViewDOM(); + let updateDiff = { + "0": ' phx-mounted="[["ignore_attrs",{"attrs":["open"]}]]"', + "1": "0", + s: [ + "
\n A\n ", + "
", + ], + }; + + let view = simulateJoinedView(el, liveSocket); + view.applyDiff("update", updateDiff, ({ diff, events }) => + view.update(diff, events), + ); + + expect(view.el.firstChild.tagName).toBe("DETAILS"); + expect(view.el.firstChild.open).toBe(true); + view.el.firstChild.open = false; + view.el.firstChild.setAttribute("data-foo", "bar"); + + // now update, the HTML patch would normally reset the open attribute + view.applyDiff("update", { "1": "1" }, ({ diff, events }) => + view.update(diff, events), + ); + // open is ignored, so it is kept as is + expect(view.el.firstChild.open).toBe(false); + // foo is not ignored, so it is reset + expect(view.el.firstChild.getAttribute("data-foo")).toBe(null); + expect(view.el.firstChild.textContent.replace(/\s+/g, "")).toEqual("A1"); + }); + test("ignore_attributes wildcard", () => { let liveSocket = new LiveSocket("/live", Socket); let el = liveViewDOM(); @@ -1031,8 +1065,10 @@ describe("View + DOM", function () { expect(view.el.firstChild.getAttribute("data-foo")).toBe("bar"); expect(view.el.firstChild.getAttribute("data-bar")).toBe("bar"); expect(view.el.firstChild.getAttribute("data-other")).toBe("also kept"); - expect(view.el.firstChild.getAttribute("data-new")).toBe("new"); expect(view.el.firstChild.textContent.replace(/\s+/g, "")).toEqual("A1"); + + // Not added for being ignored + expect(view.el.firstChild.getAttribute("data-new")).toBe(null); }); test("ignore_attributes *", () => { @@ -1072,8 +1108,10 @@ describe("View + DOM", function () { expect(view.el.firstChild.getAttribute("data-bar")).toBe("bar"); expect(view.el.firstChild.getAttribute("something")).toBe("else"); expect(view.el.firstChild.getAttribute("data-other")).toBe("also kept"); - expect(view.el.firstChild.getAttribute("data-new")).toBe("new"); expect(view.el.firstChild.textContent.replace(/\s+/g, "")).toEqual("A1"); + + // Not added for being ignored + expect(view.el.firstChild.getAttribute("data-new")).toBe(null); }); }); });