Repository: ember-template-lint/ember-template-lint
Pull request #950 (2019-11-23 15:28:14)

Implement `require-button-type` rule
Resolves https://github.com/ember-template-lint/ember-template-lint/issues/867


Patch:
diff --git a/docs/rule/require-button-type.md b/docs/rule/require-button-type.md
new file mode 100644
index 00000000..87f14b9b
--- /dev/null
+++ b/docs/rule/require-button-type.md
@@ -0,0 +1,28 @@
+## require-button-type
+
+This rule requires all `<button>` elements to have a valid `type` attribute.
+
+By default, the `type` attribute of `<button>` elements is `submit`. This can
+be very confusing, when a button component is developed in isolation without
+`type="button"`, and when inside a `<form>` element it suddenly starts to
+submit the form.
+
+This rule **forbids** the following:
+
+```hbs
+<button>Hello World!</button>
+<button type="">Hello World!</button>
+<button type="invalid">Hello World!</button>
+```
+
+This rule **allows** the following:
+
+```hbs
+<button type="button">Hello World!</button>
+<button type="submit">Hello World!</button>
+<button type="reset">Hello World!</button>
+```
+
+### References
+
+* Red [HTML spec - the button element](https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type)
diff --git a/docs/rules.md b/docs/rules.md
index 480c55a7..a0e62d73 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -46,6 +46,7 @@
 * [no-whitespace-for-layout](rule/no-whitespace-for-layout.md)
 * [no-whitespace-within-word](rule/no-whitespace-within-word.md)
 * [quotes](rule/quotes.md)
+* [require-button-type](rule/require-button-type.md)
 * [require-iframe-title](rule/require-iframe-title.md)
 * [require-valid-alt-text](rule/require-valid-alt-text.md)
 * [self-closing-void-elements](rule/self-closing-void-elements.md)
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 888ab646..8a994a52 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -50,6 +50,7 @@ module.exports = {
   'no-whitespace-for-layout': require('./lint-no-whitespace-for-layout'),
   'no-whitespace-within-word': require('./lint-no-whitespace-within-word'),
   quotes: require('./lint-quotes'),
+  'require-button-type': require('./lint-require-button-type'),
   'require-iframe-title': require('./lint-require-iframe-title'),
   'require-valid-alt-text': require('./lint-require-valid-alt-text'),
   'self-closing-void-elements': require('./lint-self-closing-void-elements'),
diff --git a/lib/rules/lint-require-button-type.js b/lib/rules/lint-require-button-type.js
new file mode 100644
index 00000000..594a2a7b
--- /dev/null
+++ b/lib/rules/lint-require-button-type.js
@@ -0,0 +1,45 @@
+'use strict';
+
+const Rule = require('./base');
+
+const ERROR_MESSAGE = 'All `<button>` elements should have a valid `type` attribute';
+
+module.exports = class RequireButtonType extends Rule {
+  logNode({ node, message }) {
+    return this.log({
+      message,
+      line: node.loc && node.loc.start.line,
+      column: node.loc && node.loc.start.column,
+      source: this.sourceForNode(node),
+    });
+  }
+
+  visitor() {
+    return {
+      ElementNode(node) {
+        let { tag, attributes } = node;
+
+        if (tag !== 'button') {
+          return;
+        }
+
+        let typeAttribute = attributes.find(it => it.name === 'type');
+        if (!typeAttribute) {
+          return this.logNode({ node, message: ERROR_MESSAGE });
+        }
+
+        let { value } = typeAttribute;
+        if (value.type !== 'TextNode') {
+          return;
+        }
+
+        let { chars } = value;
+        if (!['button', 'submit', 'reset'].includes(chars)) {
+          return this.logNode({ node, message: ERROR_MESSAGE });
+        }
+      },
+    };
+  }
+};
+
+module.exports.ERROR_MESSAGE = ERROR_MESSAGE;


Test patch:
diff --git a/test/unit/rules/lint-require-button-type-test.js b/test/unit/rules/lint-require-button-type-test.js
new file mode 100644
index 00000000..56379f87
--- /dev/null
+++ b/test/unit/rules/lint-require-button-type-test.js
@@ -0,0 +1,84 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+const { ERROR_MESSAGE } = require('../../../lib/rules/lint-require-button-type');
+
+generateRuleTests({
+  name: 'require-button-type',
+
+  config: true,
+
+  good: [
+    // valid static values
+    '<button type="button" />',
+    '<button type="button">label</button>',
+    '<button type="submit" />',
+    '<button type="reset" />',
+
+    // dynamic values
+    '<button type="{{buttonType}}" />',
+    '<button type={{buttonType}} />',
+
+    '<div/>',
+    '<div></div>',
+    '<div type="randomType"></div>',
+  ],
+
+  bad: [
+    {
+      template: '<button/>',
+
+      result: {
+        message: ERROR_MESSAGE,
+        moduleId: 'layout.hbs',
+        source: '<button/>',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<button>label</button>',
+
+      result: {
+        message: ERROR_MESSAGE,
+        moduleId: 'layout.hbs',
+        source: '<button>label</button>',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<button type="" />',
+
+      result: {
+        message: ERROR_MESSAGE,
+        moduleId: 'layout.hbs',
+        source: '<button type="" />',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<button type="foo" />',
+
+      result: {
+        message: ERROR_MESSAGE,
+        moduleId: 'layout.hbs',
+        source: '<button type="foo" />',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<button type=42 />',
+
+      result: {
+        message: ERROR_MESSAGE,
+        moduleId: 'layout.hbs',
+        source: '<button type=42 />',
+        line: 1,
+        column: 0,
+      },
+    },
+  ],
+});