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;