Skip to content

Commit

Permalink
Add new rule no-action-on-submit-button (#1931)
Browse files Browse the repository at this point in the history
Co-authored-by: Joao <joao.dasilva@qonto.eu>
Co-authored-by: Joao <joao.dasilva@qonto.com>
  • Loading branch information
3 people committed Nov 4, 2023
1 parent 0731f8a commit 2851a46
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 0 deletions.
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({
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: [
{
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}} />",
},
]
`);
},
},
],
});

0 comments on commit 2851a46

Please sign in to comment.