Skip to content

Commit c20baf3

Browse files
Merge pull request #2453 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-action-on-submit-button
Extract rule: template-no-action-on-submit-button
2 parents 71ca1a4 + fecd837 commit c20baf3

4 files changed

Lines changed: 318 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ rules in templates can be disabled with eslint directives with mustache or html
201201
| :----------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- | :- | :- | :- |
202202
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
203203
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
204+
| [template-no-action-on-submit-button](docs/rules/template-no-action-on-submit-button.md) | disallow action attribute on submit buttons | | | |
204205
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
205206
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
206207
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | |
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# ember/template-no-action-on-submit-button
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow click action on submit buttons within a form.
6+
7+
In a `<form>`, all `<button>` elements with a `type="submit"` attribute (or no `type`, since buttons default to `type="submit"`) should not have any click action. The action should be on the `<form>` element instead of directly on the button.
8+
9+
## Rule Details
10+
11+
This rule disallows:
12+
13+
- Using `{{action}}` or `{{on "click"}}` modifiers on submit buttons inside a `<form>`.
14+
- Using the HTML `action` attribute on submit buttons or `<input type="submit">` elements.
15+
16+
## Examples
17+
18+
### Incorrect
19+
20+
```hbs
21+
<form>
22+
<button type='submit' {{on 'click' this.handleClick}} />
23+
<button type='submit' {{action 'handleClick'}} />
24+
<button {{on 'click' this.handleClick}} />
25+
<button {{action 'handleClick'}} />
26+
</form>
27+
```
28+
29+
### Correct
30+
31+
```hbs
32+
<form>
33+
<button type='button' {{on 'click' this.handleClick}} />
34+
<button type='button' {{action 'handleClick'}} />
35+
<button type='submit' />
36+
<button />
37+
</form>
38+
```
39+
40+
Buttons outside a `<form>` are allowed to have click actions:
41+
42+
```hbs
43+
<button type='submit' {{on 'click' this.handleClick}} />
44+
<button type='submit' {{action 'handleClick'}} />
45+
<button {{on 'click' this.handleClick}} />
46+
<button {{action 'handleClick'}} />
47+
```
48+
49+
## Related Rules
50+
51+
- [template-no-action-modifiers](./template-no-action-modifiers.md)
52+
53+
## References
54+
55+
- [eslint-plugin-ember template-no-invalid-interactive](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-invalid-interactive.md)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
function isInsideForm(node) {
2+
let current = node.parent;
3+
while (current) {
4+
if (current.type === 'GlimmerElementNode' && current.tag === 'form') {
5+
return true;
6+
}
7+
current = current.parent;
8+
}
9+
return false;
10+
}
11+
12+
function isSubmitButton(node) {
13+
for (const attr of node.attributes || []) {
14+
if (
15+
attr.name === 'type' &&
16+
attr.value?.type === 'GlimmerTextNode' &&
17+
attr.value.chars !== 'submit'
18+
) {
19+
return false;
20+
}
21+
}
22+
return true;
23+
}
24+
25+
function hasClickHandlingModifier(node) {
26+
for (const mod of node.modifiers || []) {
27+
if (mod.path?.original === 'action') {
28+
// {{action ...}} defaults to click event
29+
const onPair = mod.hash?.pairs?.find((p) => p.key === 'on');
30+
if (!onPair) {
31+
return true;
32+
}
33+
const eventValue = onPair.value?.value ?? onPair.value?.chars;
34+
if (eventValue === 'click') {
35+
return true;
36+
}
37+
}
38+
if (mod.path?.original === 'on') {
39+
// {{on "event" handler}}
40+
if (mod.params?.length > 0 && mod.params[0].value === 'click') {
41+
return true;
42+
}
43+
}
44+
}
45+
return false;
46+
}
47+
48+
/** @type {import('eslint').Rule.RuleModule} */
49+
module.exports = {
50+
meta: {
51+
type: 'problem',
52+
docs: {
53+
description: 'disallow action attribute on submit buttons',
54+
category: 'Best Practices',
55+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-action-on-submit-button.md',
56+
templateMode: 'both',
57+
},
58+
fixable: null,
59+
schema: [],
60+
messages: {
61+
noActionOnSubmitButton:
62+
'In a `<form>`, a `<button>` with `type="submit"` should have no click action',
63+
},
64+
originallyFrom: {
65+
name: 'ember-template-lint',
66+
rule: 'lib/rules/no-action-on-submit-button.js',
67+
docs: 'docs/rule/no-action-on-submit-button.md',
68+
tests: 'test/unit/rules/no-action-on-submit-button-test.js',
69+
},
70+
},
71+
72+
create(context) {
73+
return {
74+
GlimmerElementNode(node) {
75+
if (node.tag !== 'button') {
76+
return;
77+
}
78+
79+
if (!isInsideForm(node)) {
80+
return;
81+
}
82+
83+
if (isSubmitButton(node) && hasClickHandlingModifier(node)) {
84+
context.report({
85+
node,
86+
messageId: 'noActionOnSubmitButton',
87+
});
88+
}
89+
},
90+
};
91+
},
92+
};
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
const rule = require('../../../lib/rules/template-no-action-on-submit-button');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ruleTester = new RuleTester({
5+
parser: require.resolve('ember-eslint-parser'),
6+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
7+
});
8+
9+
const ERROR_MESSAGE =
10+
'In a `<form>`, a `<button>` with `type="submit"` should have no click action';
11+
12+
ruleTester.run('template-no-action-on-submit-button', rule, {
13+
valid: [
14+
'<template>button</template>',
15+
'<template><form><button type="button" /></form></template>',
16+
'<template><form><button type="button" {{action this.handleClick}} /></form></template>',
17+
'<template><form><button type="button" {{action this.handleClick on="click"}} /></form></template>',
18+
'<template><form><button type="button" {{action this.handleMouseover on="mouseOver"}} /></form></template>',
19+
'<template><form><button type="button" {{on "click" this.handleClick}} /></form></template>',
20+
'<template><form><button type="button" {{on "mouseover" this.handleMouseover}} /></form></template>',
21+
'<template>submit</template>',
22+
'<template><form><button /></form></template>',
23+
'<template><form><button type="submit" /></form></template>',
24+
'<template><form><button type="submit" {{action this.handleMouseover on="mouseOver"}} /></form></template>',
25+
'<template><form><button type="submit" {{on "mouseover" this.handleMouseover}} /></form></template>',
26+
'<template><form><div/></form></template>',
27+
'<template><form><div></div></form></template>',
28+
'<template><form><div type="submit"></div></form></template>',
29+
'<template><form><div type="submit" {{action this.handleClick}}></div></form></template>',
30+
'<template><form><div type="submit" {{on "click" this.handleClick}}></div></form></template>',
31+
// Outside a form — valid
32+
'<template><button {{action this.handleClick}} /></template>',
33+
'<template><button {{action this.handleClick on="click"}}/></template>',
34+
'<template><button {{on "click" this.handleClick}} /></template>',
35+
'<template><button type="submit" {{action this.handleClick}} /></template>',
36+
'<template><button type="submit" {{action this.handleClick on="click"}} /></template>',
37+
'<template><button type="submit" {{action (fn this.someAction "foo")}} /></template>',
38+
'<template><button type="submit" {{on "click" this.handleClick}} /></template>',
39+
'<template><button type="submit" {{on "click" (fn this.addNumber 123)}} /></template>',
40+
],
41+
42+
invalid: [
43+
{
44+
code: '<template><form><button {{action this.handleClick}} /></form></template>',
45+
output: null,
46+
errors: [{ message: ERROR_MESSAGE }],
47+
},
48+
{
49+
code: '<template><form><button {{action this.handleClick on="click"}}/></form></template>',
50+
output: null,
51+
errors: [{ message: ERROR_MESSAGE }],
52+
},
53+
{
54+
code: '<template><form><button {{on "click" this.handleClick}} /></form></template>',
55+
output: null,
56+
errors: [{ message: ERROR_MESSAGE }],
57+
},
58+
{
59+
code: '<template><form><button type="submit" {{action this.handleClick}} /></form></template>',
60+
output: null,
61+
errors: [{ message: ERROR_MESSAGE }],
62+
},
63+
{
64+
code: '<template><form><button type="submit" {{action this.handleClick on="click"}} /></form></template>',
65+
output: null,
66+
errors: [{ message: ERROR_MESSAGE }],
67+
},
68+
{
69+
code: '<template><form><button type="submit" {{action (fn this.someAction "foo")}} /></form></template>',
70+
output: null,
71+
errors: [{ message: ERROR_MESSAGE }],
72+
},
73+
{
74+
code: '<template><form><button type="submit" {{on "click" this.handleClick}} /></form></template>',
75+
output: null,
76+
errors: [{ message: ERROR_MESSAGE }],
77+
},
78+
{
79+
code: '<template><form><button type="submit" {{on "click" (fn this.addNumber 123)}} /></form></template>',
80+
output: null,
81+
errors: [{ message: ERROR_MESSAGE }],
82+
},
83+
{
84+
code: '<template><form><div><button type="submit" {{action this.handleClick}} /></div></form></template>',
85+
output: null,
86+
errors: [{ message: ERROR_MESSAGE }],
87+
},
88+
],
89+
});
90+
91+
const hbsRuleTester = new RuleTester({
92+
parser: require.resolve('ember-eslint-parser/hbs'),
93+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
94+
});
95+
96+
hbsRuleTester.run('template-no-action-on-submit-button (hbs)', rule, {
97+
valid: [
98+
'<form><button type="button" /></form>',
99+
'<form><button type="button" {{action this.handleClick}} /></form>',
100+
'<form><button type="button" {{action this.handleClick on="click"}} /></form>',
101+
'<form><button type="button" {{action this.handleMouseover on="mouseOver"}} /></form>',
102+
'<form><button type="button" {{on "click" this.handleClick}} /></form>',
103+
'<form><button type="button" {{on "mouseover" this.handleMouseover}} /></form>',
104+
'<form><button /></form>',
105+
'<form><button type="submit" /></form>',
106+
'<form><button type="submit" {{action this.handleMouseover on="mouseOver"}} /></form>',
107+
'<form><button type="submit" {{on "mouseover" this.handleMouseover}} /></form>',
108+
'<form><div/></form>',
109+
'<form><div></div></form>',
110+
'<form><div type="submit"></div></form>',
111+
'<form><div type="submit" {{action this.handleClick}}></div></form>',
112+
'<form><div type="submit" {{on "click" this.handleClick}}></div></form>',
113+
// Outside a form — valid
114+
'<button {{action this.handleClick}} />',
115+
'<button {{action this.handleClick on="click"}}/>',
116+
'<button {{on "click" this.handleClick}} />',
117+
'<button type="submit" {{action this.handleClick}} />',
118+
'<button type="submit" {{action this.handleClick on="click"}} />',
119+
'<button type="submit" {{action (fn this.someAction "foo")}} />',
120+
'<button type="submit" {{on "click" this.handleClick}} />',
121+
'<button type="submit" {{on "click" (fn this.addNumber 123)}} />',
122+
],
123+
invalid: [
124+
{
125+
code: '<form><button {{action this.handleClick}} /></form>',
126+
output: null,
127+
errors: [{ message: ERROR_MESSAGE }],
128+
},
129+
{
130+
code: '<form><button {{action this.handleClick on="click"}}/></form>',
131+
output: null,
132+
errors: [{ message: ERROR_MESSAGE }],
133+
},
134+
{
135+
code: '<form><button {{on "click" this.handleClick}} /></form>',
136+
output: null,
137+
errors: [{ message: ERROR_MESSAGE }],
138+
},
139+
{
140+
code: '<form><button type="submit" {{action this.handleClick}} /></form>',
141+
output: null,
142+
errors: [{ message: ERROR_MESSAGE }],
143+
},
144+
{
145+
code: '<form><button type="submit" {{action this.handleClick on="click"}} /></form>',
146+
output: null,
147+
errors: [{ message: ERROR_MESSAGE }],
148+
},
149+
{
150+
code: '<form><button type="submit" {{action (fn this.someAction "foo")}} /></form>',
151+
output: null,
152+
errors: [{ message: ERROR_MESSAGE }],
153+
},
154+
{
155+
code: '<form><button type="submit" {{on "click" this.handleClick}} /></form>',
156+
output: null,
157+
errors: [{ message: ERROR_MESSAGE }],
158+
},
159+
{
160+
code: '<form><button type="submit" {{on "click" (fn this.addNumber 123)}} /></form>',
161+
output: null,
162+
errors: [{ message: ERROR_MESSAGE }],
163+
},
164+
{
165+
code: '<form><div><button type="submit" {{action this.handleClick}} /></div></form>',
166+
output: null,
167+
errors: [{ message: ERROR_MESSAGE }],
168+
},
169+
],
170+
});

0 commit comments

Comments
 (0)