Skip to content

Commit b7e4afb

Browse files
wu-s-johnsindresorhus
authored andcommitted
Add prefer-add-event-listener rule (#147)
1 parent 1d7df66 commit b7e4afb

File tree

6 files changed

+247
-2
lines changed

6 files changed

+247
-2
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Prefer `addEventListener` over `on`-functions
2+
3+
Enforces the use of, for example, `foo.addEventListener('click', handler);` over `foo.onclick = handler;` for HTML DOM Events. There are [numerous advantages of using `addEventListener`](https://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick/35093997#35093997). Some of these advantages include registering unlimited event handlers and optionally having the event handler invoked only once.
4+
5+
This rule is fixable.
6+
7+
8+
## Fail
9+
10+
```js
11+
foo.onclick = () => {};
12+
```
13+
14+
```js
15+
foo.onkeydown = () => {};
16+
```
17+
18+
```js
19+
foo.bar.onclick = onClick;
20+
```
21+
22+
## Pass
23+
24+
```js
25+
foo.addEventListener('click', () => {});
26+
```
27+
28+
```js
29+
foo.addEventListener('keydown', () => {});
30+
```
31+
32+
```js
33+
foo.bar.addEventListener('click', onClick);
34+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ module.exports = {
3434
'unicorn/regex-shorthand': 'error',
3535
'unicorn/prefer-spread': 'error',
3636
'unicorn/error-message': 'error',
37-
'unicorn/no-unsafe-regex': 'error'
37+
'unicorn/no-unsafe-regex': 'error',
38+
'unicorn/prefer-add-event-listener': 'error'
3839
}
3940
}
4041
}

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ Configure it in `package.json`.
5353
"unicorn/regex-shorthand": "error",
5454
"unicorn/prefer-spread": "error",
5555
"unicorn/error-message": "error",
56-
"unicorn/no-unsafe-regex": "error"
56+
"unicorn/no-unsafe-regex": "error",
57+
"unicorn/prefer-add-event-listener": "error"
5758
}
5859
}
5960
}
@@ -83,6 +84,7 @@ Configure it in `package.json`.
8384
- [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()`. *(fixable)*
8485
- [error-message](docs/rules/error-message.md) - Enforce passing a `message` value when throwing a built-in error.
8586
- [no-unsafe-regex](docs/rules/no-unsafe-regex.md) - Disallow unsafe regular expressions.
87+
- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `addEventListener` over `on`-functions. *(fixable)*
8688

8789

8890
## Recommended config

rules/prefer-add-event-listener.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict';
2+
const getDocsUrl = require('./utils/get-docs-url');
3+
const domEventsJson = require('./utils/dom-events.json');
4+
5+
const nestedEvents = Object.keys(domEventsJson).map(key => domEventsJson[key]);
6+
const eventTypes = new Set(nestedEvents.reduce((accEvents, events) => accEvents.concat(events), []));
7+
const getEventMethodName = memberExpression => memberExpression.property.name;
8+
const getEventTypeName = eventMethodName => eventMethodName.slice('on'.length);
9+
10+
const fix = (fixer, sourceCode, assignmentNode, memberExpression) => {
11+
const eventTypeName = getEventTypeName(getEventMethodName(memberExpression));
12+
const eventObjectCode = sourceCode.getText(memberExpression.object);
13+
const fncCode = sourceCode.getText(assignmentNode.right);
14+
const fixedCodeStatement = `${eventObjectCode}.addEventListener('${eventTypeName}', ${fncCode})`;
15+
return fixer.replaceText(assignmentNode, fixedCodeStatement);
16+
};
17+
18+
const isOnEvent = memberExpression => {
19+
if (memberExpression.type === 'MemberExpression') {
20+
const eventMethodName = getEventMethodName(memberExpression);
21+
if (eventMethodName.startsWith('on')) {
22+
return eventTypes.has(getEventTypeName(eventMethodName));
23+
}
24+
}
25+
26+
return false;
27+
};
28+
29+
const create = context => {
30+
return {
31+
AssignmentExpression(node) {
32+
const memberExpression = node.left;
33+
if (isOnEvent(memberExpression, context, node)) {
34+
context.report({
35+
node,
36+
message: `Prefer \`addEventListener\` over \`${getEventMethodName(memberExpression)}\``,
37+
fix: fixer => fix(fixer, context.getSourceCode(), node, memberExpression)
38+
});
39+
}
40+
}
41+
};
42+
};
43+
44+
module.exports = {
45+
create,
46+
meta: {
47+
docs: {
48+
url: getDocsUrl()
49+
},
50+
fixable: 'code'
51+
}
52+
};

rules/utils/dom-events.json

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{
2+
"mouse": [
3+
"click",
4+
"contextmenu",
5+
"dblclick",
6+
"mousedown",
7+
"mouseenter",
8+
"mouseleave",
9+
"mousemove",
10+
"mouseover",
11+
"mouseout",
12+
"mouseup"
13+
],
14+
"keyboard": [
15+
"keydown",
16+
"keypress",
17+
"keyup"
18+
],
19+
"frame": [
20+
"abort",
21+
"beforeunload",
22+
"error",
23+
"hashchange",
24+
"load",
25+
"pageshow",
26+
"pagehide",
27+
"resize",
28+
"scroll",
29+
"unload"
30+
],
31+
"form": [
32+
"blur",
33+
"change",
34+
"focus",
35+
"focusin",
36+
"focusout",
37+
"input",
38+
"invalid",
39+
"reset",
40+
"search",
41+
"select",
42+
"submit"
43+
],
44+
"drag": [
45+
"drag",
46+
"dragend",
47+
"dragenter",
48+
"dragleave",
49+
"dragover",
50+
"dragstart",
51+
"drop"
52+
],
53+
"clipboard": [
54+
"copy",
55+
"cut",
56+
"paste"
57+
],
58+
"print": [
59+
"afterprint",
60+
"beforeprint"
61+
],
62+
"media": [
63+
"abort",
64+
"canplay",
65+
"canplaythrough",
66+
"durationchange",
67+
"ended",
68+
"error",
69+
"loadeddata",
70+
"loadedmetadata",
71+
"loadstart",
72+
"pause",
73+
"play",
74+
"playing",
75+
"progress",
76+
"ratechange",
77+
"seeked",
78+
"seeking",
79+
"stalled",
80+
"suspend",
81+
"timeupdate",
82+
"volumechange",
83+
"waiting"
84+
],
85+
"server-sent": [
86+
"error",
87+
"message",
88+
"open"
89+
],
90+
"misc": [
91+
"wheel",
92+
"online",
93+
"offline",
94+
"show",
95+
"toggle",
96+
"wheel"
97+
]
98+
}

test/prefer-add-event-listener.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import test from 'ava';
2+
import avaRuleTester from 'eslint-ava-rule-tester';
3+
import rule from '../rules/prefer-add-event-listener';
4+
5+
const ruleTester = avaRuleTester(test, {
6+
env: {
7+
es6: true
8+
}
9+
});
10+
11+
const invalidTestCase = (code, correctCode, eventType) => {
12+
return {
13+
code,
14+
output: correctCode,
15+
errors: [{message: `Prefer \`addEventListener\` over \`${eventType}\``}]
16+
};
17+
};
18+
19+
ruleTester.run('prefer-add-event-listener', rule, {
20+
valid: [
21+
`foo.addEventListener('click', () => {})`,
22+
`foo.onclick`,
23+
`foo.setCallBack = () => {console.log('foo')}`,
24+
`setCallBack = () => {console.log('foo')}`,
25+
`foo.onclick.bar = () => {}`
26+
],
27+
invalid: [
28+
invalidTestCase(
29+
'foo.onclick = () => {}',
30+
`foo.addEventListener('click', () => {})`,
31+
'onclick'
32+
),
33+
invalidTestCase(
34+
'foo.bar.onclick = onClick',
35+
`foo.bar.addEventListener('click', onClick)`,
36+
'onclick'
37+
),
38+
invalidTestCase(
39+
'foo.onkeydown = () => {}',
40+
`foo.addEventListener('keydown', () => {})`,
41+
'onkeydown'
42+
),
43+
invalidTestCase(
44+
'foo.ondragend = () => {}',
45+
`foo.addEventListener('dragend', () => {})`,
46+
'ondragend'
47+
),
48+
invalidTestCase(
49+
`foo.onclick = function (e) {
50+
console.log(e);
51+
}`,
52+
`foo.addEventListener('click', function (e) {
53+
console.log(e);
54+
})`,
55+
'onclick'
56+
)
57+
]
58+
});

0 commit comments

Comments
 (0)