Skip to content

Commit 4a1e72d

Browse files
committed
fix(lint): support framework event bindings
Signed-off-by: John Yanarella <jyanarella@nvidia.com>
1 parent b6c09ba commit 4a1e72d

2 files changed

Lines changed: 85 additions & 3 deletions

File tree

projects/lint/src/eslint/rules/no-invalid-event-listeners.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,52 @@ describe('noInvalidEventListeners', () => {
5757
valid: [
5858
'<button @click="handleClick">Click</button>',
5959
'<button (click)="handleClick()">Click</button>',
60-
'<button v-on:click="handleClick">Click</button>'
60+
'<button v-on:click="handleClick">Click</button>',
61+
'<button onclick="${handleClick}">Click</button>',
62+
'<button onclick="{{handleClick}}">Click</button>',
63+
'<button onclick="{handleClick}">Click</button>',
64+
// React synthetic event binding
65+
'<button onClick={handleClick}>Click</button>',
66+
// React custom element native event binding
67+
`function SharedDropdown() {
68+
return (
69+
<nve-dropdown id="row-actions-dropdown" alignment="end" ontoggle={() => undefined}>
70+
<nve-menu>
71+
<nve-menu-item>action 1</nve-menu-item>
72+
<nve-menu-item>action 2</nve-menu-item>
73+
<nve-menu-item>action 3</nve-menu-item>
74+
</nve-menu>
75+
</nve-dropdown>
76+
);
77+
}`
6178
],
6279
invalid: []
6380
});
6481
});
6582

83+
it('should defer errors for incomplete event handler bindings', () => {
84+
tester.run('incomplete event handler bindings', rule, {
85+
valid: [
86+
'<button onClick=',
87+
'<button onClick={',
88+
'<button onClick={()',
89+
'<button onClick="',
90+
'<button onClick="${',
91+
'<button onClick="{{'
92+
],
93+
invalid: [
94+
{
95+
code: '<button onClick=handleClick',
96+
errors: [{ messageId: 'no-inline-event-handler', data: { attribute: 'onClick' } }]
97+
},
98+
{
99+
code: '<button onClick="handleClick',
100+
errors: [{ messageId: 'no-inline-event-handler', data: { attribute: 'onClick' } }]
101+
}
102+
]
103+
});
104+
});
105+
66106
it('should report inline onclick handler', () => {
67107
tester.run('should report inline onclick handler', rule, {
68108
valid: [],

projects/lint/src/eslint/rules/no-invalid-event-listeners.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,50 @@
33

44
import type { Rule } from 'eslint';
55
import { createVisitors } from '@html-eslint/eslint-plugin/lib/rules/utils/visitors.js';
6-
import type { HtmlTagNode } from '../rule-types.js';
6+
import type { HtmlAttribute, HtmlTagNode } from '../rule-types.js';
77

88
declare const __ELEMENTS_PAGES_BASE_URL__: string;
99
const INLINE_EVENT_HANDLER = /^on[a-z]+$/i;
10+
const DATA_BINDING_PATTERNS = [
11+
/\$\{[^}]*\}/, // ${...} - JavaScript template literals
12+
/\{\{[^}]*\}\}/, // {{...}} - Vue, Angular, Handlebars
13+
/^\{[^}]+\}$/ // {...} - JSX/React expressions (entire value is an expression)
14+
];
15+
16+
function hasDataBinding(attribute: HtmlAttribute): boolean {
17+
const value = attribute.value?.value;
18+
if (!value) return false;
19+
20+
if (DATA_BINDING_PATTERNS.some(pattern => pattern.test(value))) {
21+
return true;
22+
}
23+
24+
// NOTE: The HTML parser truncates unquoted JSX callbacks to a value beginning with "{".
25+
return !attribute.startWrapper && value.startsWith('{');
26+
}
27+
28+
// Defer lint errors while developers are typing an event binding in the editor.
29+
function isIncompleteEventHandler(attribute: HtmlAttribute, sourceText: string): boolean {
30+
if (attribute.value || !attribute.range || sourceText[attribute.range[1]] !== '=') {
31+
return false;
32+
}
33+
34+
const value = sourceText.slice(attribute.range[1] + 1);
35+
if (!value) {
36+
return true;
37+
}
38+
39+
if (value.startsWith('{') || value.startsWith('$')) {
40+
return true;
41+
}
42+
43+
if (!value.startsWith('"')) {
44+
return false;
45+
}
46+
47+
const quotedValue = value.slice(1);
48+
return !quotedValue || quotedValue.startsWith('{') || quotedValue.startsWith('$');
49+
}
1050

1151
const rule = {
1252
meta: {
@@ -24,13 +64,15 @@ const rule = {
2464
}
2565
},
2666
create(context: Rule.RuleContext) {
67+
const sourceText = context.sourceCode.getText();
68+
2769
return createVisitors(context, {
2870
Tag(node: HtmlTagNode) {
2971
(node.attributes ?? []).forEach(attr => {
3072
if (attr.type !== 'Attribute' || !attr.key?.value) return;
3173

3274
const name = attr.key.value;
33-
if (INLINE_EVENT_HANDLER.test(name)) {
75+
if (INLINE_EVENT_HANDLER.test(name) && !hasDataBinding(attr) && !isIncompleteEventHandler(attr, sourceText)) {
3476
context.report({
3577
node: attr,
3678
messageId: 'no-inline-event-handler',

0 commit comments

Comments
 (0)