Skip to content

Commit 82c9577

Browse files
committed
Extract rule: template-no-builtin-form-components
1 parent e7569a0 commit 82c9577

4 files changed

Lines changed: 230 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ rules in templates can be disabled with eslint directives with mustache or html
211211
| [template-no-bare-strings](docs/rules/template-no-bare-strings.md) | disallow bare strings in templates (require translation/localization) | | | |
212212
| [template-no-bare-yield](docs/rules/template-no-bare-yield.md) | disallow templates whose only meaningful content is a bare {{yield}} | | | |
213213
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | |
214+
| [template-no-builtin-form-components](docs/rules/template-no-builtin-form-components.md) | disallow usage of built-in form components | | | |
214215
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
215216
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
216217
| [template-no-class-bindings](docs/rules/template-no-class-bindings.md) | disallow passing classBinding or classNameBindings as arguments in templates | | | |
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# ember/template-no-builtin-form-components
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow usage of Ember's built-in `<Input>` and `<Textarea>` components. These components use two-way binding to mutate values, which is considered an anti-pattern. Use native HTML `<input>` and `<textarea>` elements instead.
6+
7+
## Examples
8+
9+
This rule **forbids** the following:
10+
11+
```gjs
12+
<template><Input @type="text" @value={{this.name}} /></template>
13+
```
14+
15+
```gjs
16+
<template><Textarea @value={{this.body}}></Textarea></template>
17+
```
18+
19+
This rule **allows** the following:
20+
21+
```gjs
22+
<template><input type="text" value={{this.name}} {{on "input" this.handleInput}} /></template>
23+
```
24+
25+
```gjs
26+
<template><textarea {{on "input" this.handleInput}}>{{this.body}}</textarea></template>
27+
```
28+
29+
## Migration
30+
31+
Many forms may be simplified by switching to a light one-way data approach.
32+
33+
For example – vanilla JavaScript has everything we need to handle form data, de-sync it from our source data and collect all user input in a single object.
34+
35+
```js
36+
import Component from '@glimmer/component';
37+
import { tracked } from '@glimmer/tracking';
38+
import { action } from '@ember/object';
39+
40+
export default class MyComponent extends Component {
41+
@tracked userInput = {};
42+
43+
@action
44+
handleInput(event) {
45+
const formData = new FormData(event.currentTarget);
46+
this.userInput = Object.fromEntries(formData.entries());
47+
}
48+
}
49+
```
50+
51+
```hbs
52+
<form {{on 'input' this.handleInput}}>
53+
<label>
54+
Name
55+
<input name='name' />
56+
</label>
57+
</form>
58+
```
59+
60+
Another option would is to "control" the field's value by replacing the built-in form component with a native HTML element and binding an event listener to handle user input.
61+
62+
In the following example the initial value of a field is controlled by a local tracked property, which is updated by an event listener.
63+
64+
```js
65+
import Component from '@glimmer/component';
66+
import { tracked } from '@glimmer/tracking';
67+
import { action } from '@ember/object';
68+
69+
export default class MyComponent extends Component {
70+
@tracked name;
71+
72+
@action
73+
updateName(event) {
74+
this.name = event.target.value;
75+
}
76+
}
77+
```
78+
79+
```hbs
80+
<input type='text' value={{this.name}} {{on 'input' this.updateName}} />
81+
```
82+
83+
## Related Rules
84+
85+
- [no-mut-helper](template-no-mut-helper.md)
86+
87+
## References
88+
89+
- [Ember Built-in Components](https://guides.emberjs.com/release/components/built-in-components/)
90+
- [ember-template-lint no-builtin-form-components](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-builtin-form-components.md)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'disallow usage of built-in form components',
7+
category: 'Best Practices',
8+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-builtin-form-components.md',
9+
templateMode: 'both',
10+
},
11+
fixable: null,
12+
schema: [],
13+
messages: {
14+
noInput:
15+
'Do not use the `Input` component. Built-in form components use two-way binding to mutate values. Instead, refactor to use a native HTML element.',
16+
noTextarea:
17+
'Do not use the `Textarea` component. Built-in form components use two-way binding to mutate values. Instead, refactor to use a native HTML element.',
18+
},
19+
originallyFrom: {
20+
name: 'ember-template-lint',
21+
rule: 'lib/rules/no-builtin-form-components.js',
22+
docs: 'docs/rule/no-builtin-form-components.md',
23+
tests: 'test/unit/rules/no-builtin-form-components-test.js',
24+
},
25+
},
26+
27+
create(context) {
28+
const MESSAGE_IDS = {
29+
Input: 'noInput',
30+
Textarea: 'noTextarea',
31+
};
32+
33+
return {
34+
GlimmerElementNode(node) {
35+
const messageId = MESSAGE_IDS[node.tag];
36+
if (messageId) {
37+
context.report({
38+
node,
39+
messageId,
40+
});
41+
}
42+
},
43+
};
44+
},
45+
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
const rule = require('../../../lib/rules/template-no-builtin-form-components');
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+
ruleTester.run('template-no-builtin-form-components', rule, {
10+
valid: [
11+
'<template><input type="text" /></template>',
12+
'<template><input type="checkbox" /></template>',
13+
'<template><input type="radio" /></template>',
14+
'<template><textarea></textarea></template>',
15+
'<template><div></div></template>',
16+
],
17+
invalid: [
18+
{
19+
code: '<template><Input /></template>',
20+
output: null,
21+
errors: [
22+
{
23+
messageId: 'noInput',
24+
},
25+
],
26+
},
27+
{
28+
code: '<template><Input type="text" /></template>',
29+
output: null,
30+
errors: [
31+
{
32+
messageId: 'noInput',
33+
},
34+
],
35+
},
36+
{
37+
code: '<template><Textarea></Textarea></template>',
38+
output: null,
39+
errors: [
40+
{
41+
messageId: 'noTextarea',
42+
},
43+
],
44+
},
45+
{
46+
code: '<template><Textarea @value={{this.body}}></Textarea></template>',
47+
output: null,
48+
errors: [
49+
{
50+
messageId: 'noTextarea',
51+
},
52+
],
53+
},
54+
],
55+
});
56+
57+
const hbsRuleTester = new RuleTester({
58+
parser: require.resolve('ember-eslint-parser/hbs'),
59+
parserOptions: {
60+
ecmaVersion: 2022,
61+
sourceType: 'module',
62+
},
63+
});
64+
65+
hbsRuleTester.run('template-no-builtin-form-components', rule, {
66+
valid: [
67+
'<input type="text" />',
68+
'<input type="checkbox" />',
69+
'<input type="radio" />',
70+
'<textarea></textarea>',
71+
],
72+
invalid: [
73+
{
74+
code: '<Input />',
75+
output: null,
76+
errors: [
77+
{
78+
message:
79+
'Do not use the `Input` component. Built-in form components use two-way binding to mutate values. Instead, refactor to use a native HTML element.',
80+
},
81+
],
82+
},
83+
{
84+
code: '<Textarea></Textarea>',
85+
output: null,
86+
errors: [
87+
{
88+
message:
89+
'Do not use the `Textarea` component. Built-in form components use two-way binding to mutate values. Instead, refactor to use a native HTML element.',
90+
},
91+
],
92+
},
93+
],
94+
});

0 commit comments

Comments
 (0)