Repository: ember-template-lint/ember-template-lint
Pull request #614 (2019-01-17 06:10:37)

feat: add new rule 'no-element-event-actions'.
## no-element-event-actions

Using HTML element event properties such as `onclick` for Ember actions is not recommended for the following reasons:

* It doesn't work for SVGs (since there is no `onclick` property of `SVGElement`).
* It can lead to confusing and unexpected behavior. For a comprehensive explanation of why, read [Deep Dive on Ember Events].

This rule **forbids** the following:

```hbs
<button onclick={{action "submit"}}>Submit</button>
```

This rule **allows** the following:

```hbs
<button {{action "submit"}}>Submit</button>
```

```hbs
<button {{action "submit" on="doubleClick"}}>Submit On Double Click</button>
```

### References

* [Deep Dive on Ember Events]
* Ember [Template Action](https://guides.emberjs.com/release/templates/actions/) documentation
* [List of DOM Events](https://developer.mozilla.org/en-US/docs/Web/Events)

[Deep Dive on Ember Events]: https://medium.com/square-corner-blog/deep-dive-on-ember-events-cf684fd3b808

bmish: @rwjblue are these correct/good examples for angle bracket component invocation? I added these.

Good: `<SiteHeader click={{action "myAction"}} @user={{this.user}} />`
Bad: `<SiteHeader onclick={{action "myAction"}} @user={{this.user}} />`

Patch:
diff --git a/docs/rule/no-element-event-actions.md b/docs/rule/no-element-event-actions.md
new file mode 100644
index 00000000..6ca87d7c
--- /dev/null
+++ b/docs/rule/no-element-event-actions.md
@@ -0,0 +1,30 @@
+## no-element-event-actions
+
+Using HTML element event properties such as `onclick` for Ember actions is not recommended for the following reasons:
+
+* It doesn't work for SVGs (since there is no `onclick` property of `SVGElement`).
+* It can lead to confusing and unexpected behavior when mixed with normal action usage. For a comprehensive explanation of why, read [Deep Dive on Ember Events].
+
+This rule **forbids** the following:
+
+```hbs
+<button onclick={{action "submit"}}>Submit</button>
+```
+
+This rule **allows** the following:
+
+```hbs
+<button {{action "submit"}}>Submit</button>
+```
+
+```hbs
+<button {{action "submit" on="doubleClick"}}>Submit On Double Click</button>
+```
+
+### References
+
+* [Deep Dive on Ember Events]
+* Ember [Template Action](https://guides.emberjs.com/release/templates/actions/) documentation
+* [List of DOM Events](https://developer.mozilla.org/en-US/docs/Web/Events)
+
+[Deep Dive on Ember Events]: https://medium.com/square-corner-blog/deep-dive-on-ember-events-cf684fd3b808
diff --git a/docs/rules.md b/docs/rules.md
index 0234df4c..9eb19e26 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -14,6 +14,7 @@
 * [no-bare-strings](rule/no-bare-strings.md)
 * [no-debugger](rule/no-debugger.md)
 * [no-duplicate-attributes](rule/no-duplicate-attributes.md)
+* [no-element-event-actions](rule/no-element-event-actions.md)
 * [no-extra-mut-helper-argument](rule/no-extra-mut-helper-argument.md)
 * [no-html-comments](rule/no-html-comments.md)
 * [no-implicit-this](rule/no-implicit-this.md)
diff --git a/lib/helpers/ast-node-info.js b/lib/helpers/ast-node-info.js
index 504a53e9..441ced36 100644
--- a/lib/helpers/ast-node-info.js
+++ b/lib/helpers/ast-node-info.js
@@ -12,6 +12,10 @@ function isTextNode(node) {
   return node.type === 'TextNode';
 }
 
+function isAttrNode(node) {
+  return node.type === 'AttrNode';
+}
+
 function isCommentStatement(node) {
   return node.type === 'CommentStatement';
 }
@@ -121,4 +125,5 @@ module.exports = {
   isMustacheStatement: isMustacheStatement,
   isNonConfigurationHtmlComment: isNonConfigurationHtmlComment,
   isTextNode: isTextNode,
+  isAttrNode: isAttrNode,
 };
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 296afac7..d94131a2 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -18,6 +18,7 @@ module.exports = {
   'no-bare-strings': require('./lint-no-bare-strings'),
   'no-debugger': require('./lint-no-debugger'),
   'no-duplicate-attributes': require('./lint-duplicate-attributes'),
+  'no-element-event-actions': require('./lint-no-element-event-actions'),
   'no-extra-mut-helper-argument': require('./lint-no-extra-mut-helper-argument'),
   'no-html-comments': require('./lint-no-html-comments'),
   'no-implicit-this': require('./lint-no-implicit-this'),
diff --git a/lib/rules/lint-no-element-event-actions.js b/lib/rules/lint-no-element-event-actions.js
new file mode 100644
index 00000000..3ec5a719
--- /dev/null
+++ b/lib/rules/lint-no-element-event-actions.js
@@ -0,0 +1,37 @@
+'use strict';
+
+const Rule = require('./base');
+const AstNodeInfo = require('../helpers/ast-node-info');
+
+const ERROR_MESSAGE = 'Do not use HTML element event properties with Ember actions.';
+
+module.exports = class NoElementEventActions extends Rule {
+  visitor() {
+    return {
+      ElementNode(node) {
+        const eventProperties = node.attributes.filter(isEventPropertyWithAction);
+        eventProperties.forEach(eventProperty =>
+          this.log({
+            message: ERROR_MESSAGE,
+            line: eventProperty.loc && eventProperty.loc.start.line,
+            column: eventProperty.loc && eventProperty.loc.start.column,
+            source: this.sourceForNode(eventProperty),
+          })
+        );
+      },
+    };
+  }
+};
+
+function isEventPropertyWithAction(node) {
+  return (
+    AstNodeInfo.isAttrNode(node) &&
+    node.name.startsWith('on') &&
+    node.name !== 'on' &&
+    AstNodeInfo.isMustacheStatement(node.value) &&
+    AstNodeInfo.isPathExpression(node.value.path) &&
+    node.value.path.original === 'action'
+  );
+}
+
+module.exports.ERROR_MESSAGE = ERROR_MESSAGE;


Test patch:
diff --git a/test/unit/rules/lint-no-element-event-actions-test.js b/test/unit/rules/lint-no-element-event-actions-test.js
new file mode 100644
index 00000000..6a229749
--- /dev/null
+++ b/test/unit/rules/lint-no-element-event-actions-test.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+const ERROR_MESSAGE = require('../../../lib/rules/lint-no-element-event-actions').ERROR_MESSAGE;
+
+generateRuleTests({
+  name: 'no-element-event-actions',
+
+  config: true,
+
+  good: [
+    '<button></button>',
+    '<button type="button" on={{action "myAction"}}></button>',
+    '<button type="button" onclick="myFunction()"></button>',
+    '<button type="button" {{action "myAction"}}></button>',
+    '<button type="button" value={{value}}></button>',
+    '{{my-component onclick=(action "myAction") someProperty=true}}',
+    '<SiteHeader @someFunction={{action "myAction"}} @user={{this.user}} />',
+  ],
+
+  bad: [
+    {
+      template:
+        '<button onclick={{action "myAction"}} onfocus={{action "myAction"}} otherProperty=true></button>',
+
+      results: [
+        {
+          message: ERROR_MESSAGE,
+          source: 'onclick={{action "myAction"}}',
+          line: 1,
+          column: 8,
+        },
+        {
+          message: ERROR_MESSAGE,
+          source: 'onfocus={{action "myAction"}}',
+          line: 1,
+          column: 38,
+        },
+      ],
+    },
+
+    {
+      template: '<SiteHeader onclick={{action "myAction"}} @user={{this.user}} />',
+
+      result: {
+        message: ERROR_MESSAGE,
+        source: 'onclick={{action "myAction"}}',
+        line: 1,
+        column: 12,
+      },
+    },
+  ],
+});