Repository: sindresorhus/eslint-plugin-unicorn (JavaScript)

Issue:
Prefer `addEventListener` over `onclick`
`addEventListener` is better for many reasons: https://stackoverflow.com/a/35093997/64949

Same with all the other `onfoo` events.

I got the idea in https://github.com/wulkano/kap/pull/255#discussion_r147623982.

---

## Fail

```js
foo.onclick = () => {

};
```

## Pass

```js
foo.addEventListener('click', () => {

});
```

---

This should be auto-fixable.


Discussion:
@kevva @SamVerschueren Thoughts?
Yes totally agree. Let's do this one!
💯
Can I work on this?
@johnwu93 Yes :)
Do you guys just want this to be applicable whenever an object calls `onclick`, regardless of its type? Or, do you guys want this to be applicable if the object is of type `DOM element`? The later would be nice, but much more difficult to implement.
@johnwu93 ESLint doesn't have type inference, so that is impossible. We'll just assume `onclick` means it's a DOM element.



Patch:
diff --git a/docs/rules/prefer-add-event-listener.md b/docs/rules/prefer-add-event-listener.md
new file mode 100644
index 0000000000..fd05cc6d59
--- /dev/null
+++ b/docs/rules/prefer-add-event-listener.md
@@ -0,0 +1,34 @@
+# Prefer `addEventListener` over `on`-functions
+
+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.
+
+This rule is fixable.
+
+
+## Fail
+
+```js
+foo.onclick = () => {};
+```
+
+```js
+foo.onkeydown = () => {};
+```
+
+```js
+foo.bar.onclick = onClick;
+```
+
+## Pass
+
+```js
+foo.addEventListener('click', () => {});
+```
+
+```js
+foo.addEventListener('keydown', () => {});
+```
+
+```js
+foo.bar.addEventListener('click', onClick);
+```
diff --git a/index.js b/index.js
index 7711864297..56b942179e 100644
--- a/index.js
+++ b/index.js
@@ -34,7 +34,8 @@ module.exports = {
 				'unicorn/regex-shorthand': 'error',
 				'unicorn/prefer-spread': 'error',
 				'unicorn/error-message': 'error',
-				'unicorn/no-unsafe-regex': 'error'
+				'unicorn/no-unsafe-regex': 'error',
+				'unicorn/prefer-add-event-listener': 'error'
 			}
 		}
 	}
diff --git a/readme.md b/readme.md
index b77df4236e..d30a4cc231 100644
--- a/readme.md
+++ b/readme.md
@@ -53,7 +53,8 @@ Configure it in `package.json`.
 			"unicorn/regex-shorthand": "error",
 			"unicorn/prefer-spread": "error",
 			"unicorn/error-message": "error",
-			"unicorn/no-unsafe-regex": "error"
+			"unicorn/no-unsafe-regex": "error",
+			"unicorn/prefer-add-event-listener": "error"
 		}
 	}
 }
@@ -83,6 +84,7 @@ Configure it in `package.json`.
 - [prefer-spread](docs/rules/prefer-spread.md) - Prefer the spread operator over `Array.from()`. *(fixable)*
 - [error-message](docs/rules/error-message.md) - Enforce passing a `message` value when throwing a built-in error.
 - [no-unsafe-regex](docs/rules/no-unsafe-regex.md) - Disallow unsafe regular expressions.
+- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `addEventListener` over `on`-functions. *(fixable)*
 
 
 ## Recommended config
diff --git a/rules/prefer-add-event-listener.js b/rules/prefer-add-event-listener.js
new file mode 100644
index 0000000000..2078d533e9
--- /dev/null
+++ b/rules/prefer-add-event-listener.js
@@ -0,0 +1,52 @@
+'use strict';
+const getDocsUrl = require('./utils/get-docs-url');
+const domEventsJson = require('./utils/dom-events.json');
+
+const nestedEvents = Object.keys(domEventsJson).map(key => domEventsJson[key]);
+const eventTypes = new Set(nestedEvents.reduce((accEvents, events) => accEvents.concat(events), []));
+const getEventMethodName = memberExpression => memberExpression.property.name;
+const getEventTypeName = eventMethodName => eventMethodName.slice('on'.length);
+
+const fix = (fixer, sourceCode, assignmentNode, memberExpression) => {
+	const eventTypeName = getEventTypeName(getEventMethodName(memberExpression));
+	const eventObjectCode = sourceCode.getText(memberExpression.object);
+	const fncCode = sourceCode.getText(assignmentNode.right);
+	const fixedCodeStatement = `${eventObjectCode}.addEventListener('${eventTypeName}', ${fncCode})`;
+	return fixer.replaceText(assignmentNode, fixedCodeStatement);
+};
+
+const isOnEvent = memberExpression => {
+	if (memberExpression.type === 'MemberExpression') {
+		const eventMethodName = getEventMethodName(memberExpression);
+		if (eventMethodName.startsWith('on')) {
+			return eventTypes.has(getEventTypeName(eventMethodName));
+		}
+	}
+
+	return false;
+};
+
+const create = context => {
+	return {
+		AssignmentExpression(node) {
+			const memberExpression = node.left;
+			if (isOnEvent(memberExpression, context, node)) {
+				context.report({
+					node,
+					message: `Prefer \`addEventListener\` over \`${getEventMethodName(memberExpression)}\``,
+					fix: fixer => fix(fixer, context.getSourceCode(), node, memberExpression)
+				});
+			}
+		}
+	};
+};
+
+module.exports = {
+	create,
+	meta: {
+		docs: {
+			url: getDocsUrl()
+		},
+		fixable: 'code'
+	}
+};
diff --git a/rules/utils/dom-events.json b/rules/utils/dom-events.json
new file mode 100644
index 0000000000..d21ff4c0e8
--- /dev/null
+++ b/rules/utils/dom-events.json
@@ -0,0 +1,98 @@
+{
+	"mouse": [
+		"click",
+		"contextmenu",
+		"dblclick",
+		"mousedown",
+		"mouseenter",
+		"mouseleave",
+		"mousemove",
+		"mouseover",
+		"mouseout",
+		"mouseup"
+	],
+	"keyboard": [
+		"keydown",
+		"keypress",
+		"keyup"
+	],
+	"frame": [
+		"abort",
+		"beforeunload",
+		"error",
+		"hashchange",
+		"load",
+		"pageshow",
+		"pagehide",
+		"resize",
+		"scroll",
+		"unload"
+	],
+	"form": [
+		"blur",
+		"change",
+		"focus",
+		"focusin",
+		"focusout",
+		"input",
+		"invalid",
+		"reset",
+		"search",
+		"select",
+		"submit"
+	],
+	"drag": [
+		"drag",
+		"dragend",
+		"dragenter",
+		"dragleave",
+		"dragover",
+		"dragstart",
+		"drop"
+	],
+	"clipboard": [
+		"copy",
+		"cut",
+		"paste"
+	],
+	"print": [
+		"afterprint",
+		"beforeprint"
+	],
+	"media": [
+		"abort",
+		"canplay",
+		"canplaythrough",
+		"durationchange",
+		"ended",
+		"error",
+		"loadeddata",
+		"loadedmetadata",
+		"loadstart",
+		"pause",
+		"play",
+		"playing",
+		"progress",
+		"ratechange",
+		"seeked",
+		"seeking",
+		"stalled",
+		"suspend",
+		"timeupdate",
+		"volumechange",
+		"waiting"
+	],
+	"server-sent": [
+		"error",
+		"message",
+		"open"
+	],
+	"misc": [
+		"wheel",
+		"online",
+		"offline",
+		"show",
+		"toggle",
+		"wheel"
+	]
+}


Test patch:
diff --git a/test/prefer-add-event-listener.js b/test/prefer-add-event-listener.js
new file mode 100644
index 0000000000..c19a6b7587
--- /dev/null
+++ b/test/prefer-add-event-listener.js
@@ -0,0 +1,58 @@
+import test from 'ava';
+import avaRuleTester from 'eslint-ava-rule-tester';
+import rule from '../rules/prefer-add-event-listener';
+
+const ruleTester = avaRuleTester(test, {
+	env: {
+		es6: true
+	}
+});
+
+const invalidTestCase = (code, correctCode, eventType) => {
+	return {
+		code,
+		output: correctCode,
+		errors: [{message: `Prefer \`addEventListener\` over \`${eventType}\``}]
+	};
+};
+
+ruleTester.run('prefer-add-event-listener', rule, {
+	valid: [
+		`foo.addEventListener('click', () => {})`,
+		`foo.onclick`,
+		`foo.setCallBack = () => {console.log('foo')}`,
+		`setCallBack = () => {console.log('foo')}`,
+		`foo.onclick.bar = () => {}`
+	],
+	invalid: [
+		invalidTestCase(
+			'foo.onclick = () => {}',
+			`foo.addEventListener('click', () => {})`,
+			'onclick'
+		),
+		invalidTestCase(
+			'foo.bar.onclick = onClick',
+			`foo.bar.addEventListener('click', onClick)`,
+			'onclick'
+		),
+		invalidTestCase(
+			'foo.onkeydown = () => {}',
+			`foo.addEventListener('keydown', () => {})`,
+			'onkeydown'
+		),
+		invalidTestCase(
+			'foo.ondragend = () => {}',
+			`foo.addEventListener('dragend', () => {})`,
+			'ondragend'
+		),
+		invalidTestCase(
+			`foo.onclick = function (e) {
+				console.log(e);
+			}`,
+			`foo.addEventListener('click', function (e) {
+				console.log(e);
+			})`,
+			'onclick'
+		)
+	]
+});