From d149214c938b8a528c5bd87e0c8fd54ccef813fa Mon Sep 17 00:00:00 2001 From: Nanook Claw Date: Sat, 23 May 2026 16:05:53 +0000 Subject: [PATCH] fix: treat on as a normal attribute Signed-off-by: Nanook Claw --- src/dom-parts/AttributePart.js | 4 +++- src/dom-parts/TemplateResult.js | 4 +++- test/unit/template-bindings.test.js | 31 +++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/dom-parts/AttributePart.js b/src/dom-parts/AttributePart.js index f7b71e5..7ad8633 100644 --- a/src/dom-parts/AttributePart.js +++ b/src/dom-parts/AttributePart.js @@ -83,7 +83,9 @@ export const processAttributePart = (node, name) => { } // event attribute: @event=${...} || "old school" event attribute: onevent=${...} - if (name.startsWith('@') || name.startsWith('on')) { + // Note: require at least one character after "on" so that a plain `on` attribute + // is treated as a regular string attribute (see #163). + if (name.startsWith('@') || (name.startsWith('on') && name.length > 2)) { return processEventAttribute(node, name); } diff --git a/src/dom-parts/TemplateResult.js b/src/dom-parts/TemplateResult.js index df7c241..c082c96 100644 --- a/src/dom-parts/TemplateResult.js +++ b/src/dom-parts/TemplateResult.js @@ -297,7 +297,9 @@ export class TemplateResult { ]; } - if (name.startsWith('@') || name.startsWith('on')) { + // Note: require at least one character after "on" so that a plain `on` + // attribute is treated as a regular string attribute (see #163). + if (name.startsWith('@') || (name.startsWith('on') && name.length > 2)) { return [ { type: 'attribute', diff --git a/test/unit/template-bindings.test.js b/test/unit/template-bindings.test.js index 536f45e..21c4a5a 100644 --- a/test/unit/template-bindings.test.js +++ b/test/unit/template-bindings.test.js @@ -303,6 +303,37 @@ describe(`template bindings for rendering TemplateResults client side and server assert.equal(el.bar, 'baz'); }); + // Regression for #163: an attribute literally named `on` must not be routed + // through the event-binding code path (which would call addEventListener with + // an empty event type and a non-function listener). + it('treats a plain `on` attribute as a string attribute, not an event', async () => { + const el = document.createElement('div'); + const value = 'default'; + const templateResult = html``; + render(templateResult, el); + assert.equal(stripCommentMarkers(el.innerHTML), ''); + assert.equal( + stripCommentMarkers(el.innerHTML), + stripCommentMarkers(templateResult.toString()), + 'CSR template does not match SSR template', + ); + + const button = el.querySelector('button'); + assert.equal(button.getAttribute('on'), 'default'); + }); + + it('updates a plain `on` attribute on rerender', async () => { + const el = document.createElement('div'); + render(html``, el); + assert.equal(el.querySelector('button').getAttribute('on'), 'default'); + + render(html``, el); + assert.equal(el.querySelector('button').getAttribute('on'), ''); + + render(html``, el); + assert.equal(el.querySelector('button').getAttribute('on'), 'other'); + }); + it('can render conditional nested html templates', async () => { const el = document.createElement('div'); const nested = true;