Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new rule no-action-on-submit-button #1931

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Each rule has emojis denoting:
| [no-accesskey-attribute](./docs/rule/no-accesskey-attribute.md) | ✅ | | ⌨️ | 🔧 |
| [no-action](./docs/rule/no-action.md) | ✅ | | | |
| [no-action-modifiers](./docs/rule/no-action-modifiers.md) | | | | |
| [no-action-on-submit-button](./docs/rule/no-action-on-submit-button.md) | | | | |
| [no-args-paths](./docs/rule/no-args-paths.md) | ✅ | | | |
| [no-arguments-for-html-elements](./docs/rule/no-arguments-for-html-elements.md) | ✅ | | | |
| [no-aria-hidden-body](./docs/rule/no-aria-hidden-body.md) | ✅ | | ⌨️ | 🔧 |
Expand Down
46 changes: 46 additions & 0 deletions docs/rule/no-action-on-submit-button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# no-action-on-submit-button

In a `<form>`, this rule requires all `<button>` elements with a `type="submit"` attribute to not have any click action.

When the `type` attribute of `<button>` elements is `submit`, the action should be on the `<form>` element instead of directly on the button.

By default, the `type` attribute of `<button>` elements is `submit`.

## Examples

This rule **forbids** the following:

```hbs
<form>
<button type='submit' {{on 'click' this.handleClick}} />
<button type='submit' {{action 'handleClick'}} />
<button {{on 'click' this.handleClick}} />
<button {{action 'handleClick'}} />
</form>
```

This rule **allows** the following:

```hbs
// In a <form>
<form>
<button type='button' {{on 'click' this.handleClick}} />
<button type='button' {{action 'handleClick'}} />
<button type='submit' />
<button />
</form>

// Outside a <form>
<button type='submit' {{on 'click' this.handleClick}} />
<button type='submit' {{action 'handleClick'}} />
<button {{on 'click' this.handleClick}} />
<button {{action 'handleClick'}} />
```

## Related Rules

- [require-button-type](require-button-type.md)

## References

- [HTML spec - the button element](https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type)
2 changes: 2 additions & 0 deletions lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import modifiernamecase from './modifier-name-case.js';
import noabstractroles from './no-abstract-roles.js';
import noaccesskeyattribute from './no-accesskey-attribute.js';
import noactionmodifiers from './no-action-modifiers.js';
import noactiononsubmitbutton from './no-action-on-submit-button.js';
import noaction from './no-action.js';
import noargspaths from './no-args-paths.js';
import noargumentsforhtmlelements from './no-arguments-for-html-elements.js';
Expand Down Expand Up @@ -135,6 +136,7 @@ export default {
'no-abstract-roles': noabstractroles,
'no-accesskey-attribute': noaccesskeyattribute,
'no-action-modifiers': noactionmodifiers,
'no-action-on-submit-button': noactiononsubmitbutton,
'no-action': noaction,
'no-args-paths': noargspaths,
'no-arguments-for-html-elements': noargumentsforhtmlelements,
Expand Down
82 changes: 82 additions & 0 deletions lib/rules/no-action-on-submit-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Rule from './_base.js';
import hasParentTag from '../helpers/has-parent-tag.js';

const ERROR_MESSAGE =
'In a `<form>`, a `<button>` with `type="submit"` should have no click action';

export default class NoActionOnSubmitButton extends Rule {
logNode({ node, message }) {
return this.log({
bmish marked this conversation as resolved.
Show resolved Hide resolved
node,
message,
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source: this.sourceForNode(node),
});
}

visitor() {
function isTypeAttribute(attribute) {
return attribute.name === 'type';
}

function isOnClickModifier(modifier) {
let { path, params } = modifier;

return (
path.original === 'on' &&
params.length > 0 &&
params[0].type === 'StringLiteral' &&
params[0].value === 'click'
);
}

function isOnClickParameter(parameter) {
return parameter.key === 'on' && parameter.value.original === 'click';
}

function isDisallowedActionModifier(modifier) {
let { path, hash } = modifier;

let noParameter = hash.pairs.length === 0;
let onClickParameter = hash.pairs.find(isOnClickParameter);

return path.original === 'action' && (noParameter || onClickParameter);
}

return {
ElementNode(node, path) {
let { tag, attributes, modifiers } = node;

if (tag !== 'button') {
return;
}

// is this button in a <form>?
if (!hasParentTag(path, 'form')) {
return;
}

let typeAttribute = attributes.find(isTypeAttribute);
let onClickModifier = modifiers.find(isOnClickModifier);
let actionModifier = modifiers.find(isDisallowedActionModifier);

// undefined button type fallbacks on "submit"
if (!typeAttribute) {
if (actionModifier || onClickModifier) {
return this.logNode({ node, message: ERROR_MESSAGE });
}
return;
}

let { type, chars } = typeAttribute.value;

if (type === 'TextNode' && chars === 'submit') {
if (actionModifier || onClickModifier) {
return this.logNode({ node, message: ERROR_MESSAGE });
}
}
},
};
}
}
223 changes: 223 additions & 0 deletions test/unit/rules/no-action-on-submit-button-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import generateRuleTests from '../../helpers/rule-test-harness.js';

generateRuleTests({
name: 'no-action-on-submit-button',

config: true,

good: [
// valid buttons with "button" type, in a form
'<form><button type="button" /></form>',
'<form><button type="button" {{action this.handleClick}} /></form>',
'<form><button type="button" {{action this.handleClick on="click"}} /></form>',
'<form><button type="button" {{action this.handleMouseover on="mouseOver"}} /></form>',
'<form><button type="button" {{on "click" this.handleClick}} /></form>',
'<form><button type="button" {{on "mouseover" this.handleMouseover}} /></form>',

// valid buttons with "submit" type, in a form
'<form><button /></form>',
'<form><button type="submit" /></form>',
'<form><button type="submit" {{action this.handleMouseover on="mouseOver"}} /></form>',
'<form><button type="submit" {{on "mouseover" this.handleMouseover}} /></form>',

// valid div elements, in a form
'<form><div/></form>',
'<form><div></div></form>',
'<form><div type="submit"></div></form>',
'<form><div type="submit" {{action this.handleClick}}></div></form>',
'<form><div type="submit" {{on "click" this.handleClick}}></div></form>',

// valid buttons, only outside a form
'<button {{action this.handleClick}} />',
'<button {{action this.handleClick on="click"}}/>',
'<button {{on "click" this.handleClick}} />',
'<button type="submit" {{action this.handleClick}} />',
'<button type="submit" {{action this.handleClick on="click"}} />',
'<button type="submit" {{action (fn this.someAction "foo")}} />',
'<button type="submit" {{on "click" this.handleClick}} />',
'<button type="submit" {{on "click" (fn this.addNumber 123)}} />',
],

bad: [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we add a test for a deeply-nested button like this to make sure we aren't only checking the direct parent?

  • <form><div><button {{action this.handleClick}} /></div></form>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I added it as last scenario ✅

{
template: '<form><button {{action this.handleClick}} /></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 6,
"endColumn": 44,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button {{action this.handleClick}} />",
},
]
`);
},
},
{
template: '<form><button {{action this.handleClick on="click"}}/></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 6,
"endColumn": 54,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button {{action this.handleClick on=\\"click\\"}}/>",
},
]
`);
},
},
{
template: '<form><button {{on "click" this.handleClick}} /></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 6,
"endColumn": 48,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button {{on \\"click\\" this.handleClick}} />",
},
]
`);
},
},
{
template: '<form><button type="submit" {{action this.handleClick}} /></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 6,
"endColumn": 58,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button type=\\"submit\\" {{action this.handleClick}} />",
},
]
`);
},
},
{
template: '<form><button type="submit" {{action this.handleClick on="click"}} /></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 6,
"endColumn": 69,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button type=\\"submit\\" {{action this.handleClick on=\\"click\\"}} />",
},
]
`);
},
},
{
template: '<form><button type="submit" {{action (fn this.someAction "foo")}} /></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 6,
"endColumn": 68,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button type=\\"submit\\" {{action (fn this.someAction \\"foo\\")}} />",
},
]
`);
},
},
{
template: '<form><button type="submit" {{on "click" this.handleClick}} /></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 6,
"endColumn": 62,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button type=\\"submit\\" {{on \\"click\\" this.handleClick}} />",
},
]
`);
},
},
{
template: '<form><button type="submit" {{on "click" (fn this.addNumber 123)}} /></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 6,
"endColumn": 69,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button type=\\"submit\\" {{on \\"click\\" (fn this.addNumber 123)}} />",
},
]
`);
},
},
{
template: '<form><div><button type="submit" {{action this.handleClick}} /></div></form>',
verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
[
{
"column": 11,
"endColumn": 63,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "In a \`<form>\`, a \`<button>\` with \`type=\\"submit\\"\` should have no click action",
"rule": "no-action-on-submit-button",
"severity": 2,
"source": "<button type=\\"submit\\" {{action this.handleClick}} />",
},
]
`);
},
},
],
});