Skip to content

Commit 845ed49

Browse files
committed
Extract rule: template-no-passed-in-event-handlers
1 parent 8952249 commit 845ed49

4 files changed

Lines changed: 436 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ rules in templates can be disabled with eslint directives with mustache or html
259259
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
260260
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
261261
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
262+
| [template-no-passed-in-event-handlers](docs/rules/template-no-passed-in-event-handlers.md) | disallow passing event handlers directly as component arguments | | | |
262263
| [template-no-positional-data-test-selectors](docs/rules/template-no-positional-data-test-selectors.md) | disallow positional data-test-* params in curly invocations | | | |
263264
| [template-no-potential-path-strings](docs/rules/template-no-potential-path-strings.md) | disallow potential path strings in attribute values | | | |
264265
| [template-no-redundant-fn](docs/rules/template-no-redundant-fn.md) | disallow unnecessary usage of (fn) helper | | | |
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# ember/template-no-passed-in-event-handlers
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallows passing Ember event handler names directly as component arguments.
6+
7+
Passing Ember DOM event names as component arguments (e.g., `@click`, `@submit`, `@keyDown`) is discouraged. Use the `{{on}}` modifier on the element instead for explicit event binding.
8+
9+
## Rule Details
10+
11+
This rule detects component arguments whose name (without the `@` prefix) matches an Ember DOM event name. Only PascalCase component invocations are checked (built-in `Input` and `Textarea` are excluded).
12+
13+
The checked event names are:
14+
15+
`touchStart`, `touchMove`, `touchEnd`, `touchCancel`, `keyDown`, `keyUp`, `keyPress`, `mouseDown`, `mouseUp`, `contextMenu`, `click`, `doubleClick`, `mouseMove`, `mouseEnter`, `mouseLeave`, `focusIn`, `focusOut`, `submit`, `change`, `input`, `dragStart`, `drag`, `dragEnter`, `dragLeave`, `dragOver`, `dragEnd`, `drop`
16+
17+
## Examples
18+
19+
Examples of **incorrect** code for this rule:
20+
21+
```gjs
22+
<template>
23+
<MyComponent @click={{this.handleClick}} />
24+
</template>
25+
```
26+
27+
```gjs
28+
<template>
29+
<MyComponent @submit={{this.handleSubmit}} />
30+
</template>
31+
```
32+
33+
```gjs
34+
<template>
35+
<CustomButton @mouseEnter={{this.handleHover}} />
36+
</template>
37+
```
38+
39+
Examples of **correct** code for this rule:
40+
41+
```gjs
42+
<template>
43+
<MyComponent @action={{this.handleAction}} />
44+
</template>
45+
```
46+
47+
```gjs
48+
<template>
49+
<button {{on "click" this.handleClick}}>Click</button>
50+
</template>
51+
```
52+
53+
```gjs
54+
<template>
55+
<MyComponent @value={{this.value}} @onChange={{this.updateValue}} />
56+
</template>
57+
```
58+
59+
## Options
60+
61+
| Name | Type | Default | Description |
62+
| -------- | -------- | ------- | -------------------------------------------------------------------------------------------------- |
63+
| `ignore` | `object` | `{}` | Per-component exceptions. Keys are component names, values are arrays of argument names to ignore. |
64+
65+
```js
66+
module.exports = {
67+
rules: {
68+
'ember/template-no-passed-in-event-handlers': [
69+
'error',
70+
{
71+
ignore: {
72+
MyComponent: ['@click', '@submit'],
73+
},
74+
},
75+
],
76+
},
77+
};
78+
```
79+
80+
## Migration
81+
82+
Replace:
83+
84+
```gjs
85+
<MyComponent @click={{this.handleClick}} />
86+
```
87+
88+
With:
89+
90+
```gjs
91+
<MyComponent>
92+
<button {{on "click" this.handleClick}}>Click</button>
93+
</MyComponent>
94+
```
95+
96+
## References
97+
98+
- [eslint-plugin-ember template-no-passed-in-event-handlers](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-passed-in-event-handlers.md)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Comprehensive Ember event handler names
2+
const EMBER_EVENTS = new Set([
3+
'touchStart',
4+
'touchMove',
5+
'touchEnd',
6+
'touchCancel',
7+
'keyDown',
8+
'keyUp',
9+
'keyPress',
10+
'mouseDown',
11+
'mouseUp',
12+
'contextMenu',
13+
'click',
14+
'doubleClick',
15+
'mouseMove',
16+
'mouseEnter',
17+
'mouseLeave',
18+
'focusIn',
19+
'focusOut',
20+
'submit',
21+
'change',
22+
'input',
23+
'dragStart',
24+
'drag',
25+
'dragEnter',
26+
'dragLeave',
27+
'dragOver',
28+
'dragEnd',
29+
'drop',
30+
]);
31+
32+
function isEventName(name) {
33+
return EMBER_EVENTS.has(name);
34+
}
35+
36+
/** @type {import('eslint').Rule.RuleModule} */
37+
module.exports = {
38+
meta: {
39+
type: 'suggestion',
40+
docs: {
41+
description: 'disallow passing event handlers directly as component arguments',
42+
category: 'Best Practices',
43+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-passed-in-event-handlers.md',
44+
templateMode: 'both',
45+
},
46+
fixable: null,
47+
schema: [
48+
{
49+
type: 'object',
50+
properties: {
51+
ignore: {
52+
type: 'object',
53+
additionalProperties: {
54+
type: 'array',
55+
items: { type: 'string' },
56+
},
57+
},
58+
},
59+
additionalProperties: false,
60+
},
61+
],
62+
messages: {
63+
unexpected:
64+
'Event handler "@{{name}}" should not be passed as a component argument. Use the `on` modifier instead.',
65+
},
66+
originallyFrom: {
67+
name: 'ember-template-lint',
68+
rule: 'lib/rules/no-passed-in-event-handlers.js',
69+
docs: 'docs/rule/no-passed-in-event-handlers.md',
70+
tests: 'test/unit/rules/no-passed-in-event-handlers-test.js',
71+
},
72+
},
73+
74+
create(context) {
75+
const options = context.options[0] || {};
76+
const ignoreConfig = options.ignore || {};
77+
78+
return {
79+
GlimmerElementNode(node) {
80+
// Only check component invocations (PascalCase)
81+
if (!/^[A-Z]/.test(node.tag)) {
82+
return;
83+
}
84+
// Skip built-in Input/Textarea
85+
if (node.tag === 'Input' || node.tag === 'Textarea') {
86+
return;
87+
}
88+
89+
if (!node.attributes) {
90+
return;
91+
}
92+
93+
const ignoredAttrs = ignoreConfig[node.tag] || [];
94+
95+
for (const attr of node.attributes) {
96+
if (!attr.name || !attr.name.startsWith('@')) {
97+
continue;
98+
}
99+
const argName = attr.name.slice(1);
100+
101+
// Check ignore config
102+
if (ignoredAttrs.includes(attr.name)) {
103+
continue;
104+
}
105+
106+
if (isEventName(argName)) {
107+
context.report({
108+
node: attr,
109+
messageId: 'unexpected',
110+
data: { name: argName },
111+
});
112+
}
113+
}
114+
},
115+
116+
GlimmerMustacheStatement(node) {
117+
const path = node.path;
118+
if (!path || path.type !== 'GlimmerPathExpression') {
119+
return;
120+
}
121+
// Skip built-in input/textarea
122+
if (path.original === 'input' || path.original === 'textarea') {
123+
return;
124+
}
125+
// Check hash pairs for event handler names
126+
if (!node.hash || !node.hash.pairs) {
127+
return;
128+
}
129+
for (const pair of node.hash.pairs) {
130+
if (isEventName(pair.key)) {
131+
context.report({
132+
node: pair,
133+
messageId: 'unexpected',
134+
data: { name: pair.key },
135+
});
136+
}
137+
}
138+
},
139+
};
140+
},
141+
};

0 commit comments

Comments
 (0)