-
Notifications
You must be signed in to change notification settings - Fork 235
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new rule
no-action-on-submit-button
(#1931)
Co-authored-by: Joao <joao.dasilva@qonto.eu> Co-authored-by: Joao <joao.dasilva@qonto.com>
- Loading branch information
1 parent
0731f8a
commit 2851a46
Showing
5 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} | ||
} | ||
}, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}} />", | ||
}, | ||
] | ||
`); | ||
}, | ||
}, | ||
], | ||
}); |