Skip to content

Commit

Permalink
Add rule no-builtin-form-components
Browse files Browse the repository at this point in the history
  • Loading branch information
gilest committed Nov 8, 2023
1 parent 299841a commit 312ddee
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Each rule has emojis denoting:
| [no-autofocus-attribute](./docs/rule/no-autofocus-attribute.md) || | ⌨️ | |
| [no-bare-strings](./docs/rule/no-bare-strings.md) | | | | |
| [no-block-params-for-html-elements](./docs/rule/no-block-params-for-html-elements.md) || | | |
| [no-builtin-form-components](./docs/rule/no-builtin-form-components.md) | | | | |
| [no-capital-arguments](./docs/rule/no-capital-arguments.md) || | | |
| [no-class-bindings](./docs/rule/no-class-bindings.md) || | | |
| [no-curly-component-invocation](./docs/rule/no-curly-component-invocation.md) || | | 🔧 |
Expand Down
65 changes: 65 additions & 0 deletions docs/rule/no-builtin-form-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# no-builtin-form-components

Ember's built-in form components use two-way data binding, where the property as `@value` or `@checked` is mutated by user interaction. This goes against the Data Down Actions Up principle, goes against Glimmer Components’ intention to have immutable arguments, and is [discouraged by the Ember Core team](https://www.pzuraq.com/on-mut-and-2-way-binding/).

## Examples

This rule **forbids** the following:

```hbs
<Input />
```

```hbs
<Textarea></Textarea>
```

## Migration

The migration path typically involves replacing the built-in form component with a native HTML element and binding an event listener to handle user input.

```js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class MyComponent extends Component {
@tracked name;

@action
updateName(event) {
this.name = event.target.value;
}
}
```

```hbs
<input
type="text"
value={{this.name}}
{{on "input" this.updateName}}
/>
```

You may consider composing the [set helper](https://github.com/pzuraq/ember-set-helper) with the [pick helper](https://github.com/DockYard/ember-composable-helpers#pick) to avoid creating an action within a component class.

```hbs
<input
type="text"
value={{this.name}}
{{on "input" (pick "target.value" (set this "name"))}}
/>
```

## Related Rules

* [no-mut-helper](no-mut-helper.md)

## References

* [Built-in components guides](https://guides.emberjs.com/release/components/built-in-components/)
* [Built-in `Input` component API](https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/Input?anchor=Input)
* [Built-in `Textarea` component API](https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/Textarea?anchor=Textarea)
* [Native HTML `input`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)
* [Native HTML `textarea`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)
* [The `on` modifier](https://guides.emberjs.com/release/components/component-state-and-actions/#toc_html-modifiers-and-actions)
2 changes: 2 additions & 0 deletions lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import noattrsincomponents from './no-attrs-in-components.js';
import noautofocusattribute from './no-autofocus-attribute.js';
import nobarestrings from './no-bare-strings.js';
import noblockparamsforhtmlelements from './no-block-params-for-html-elements.js';
import nobuiltinformcomponents from './no-builtin-form-components.js';
import nocapitalarguments from './no-capital-arguments.js';
import noclassbindings from './no-class-bindings.js';
import nocurlycomponentinvocation from './no-curly-component-invocation.js';
Expand Down Expand Up @@ -148,6 +149,7 @@ export default {
'no-autofocus-attribute': noautofocusattribute,
'no-bare-strings': nobarestrings,
'no-block-params-for-html-elements': noblockparamsforhtmlelements,
'no-builtin-form-components': nobuiltinformcomponents,
'no-capital-arguments': nocapitalarguments,
'no-class-bindings': noclassbindings,
'no-curly-component-invocation': nocurlycomponentinvocation,
Expand Down
25 changes: 25 additions & 0 deletions lib/rules/no-builtin-form-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Rule from './_base.js';

const WHY = 'Built-in form components use two-way binding to mutate values.';
const ACTION = 'Instead, refactor to use a native HTML element.';
export const MESSAGES = {
Input: `Do not use the \`Input\` component. ${WHY} ${ACTION}`,
Textarea: `Do not use the \`Textarea\` component. ${WHY} ${ACTION}`,
};

const COMPONENTS = new Set(['Input', 'Textarea']);

export default class NoBuiltinFormComponents extends Rule {
visitor() {
return {
ElementNode(node) {
if (COMPONENTS.has(node.tag)) {
this.log({
message: MESSAGES[node.tag],
node,
});
}
},
};
}
}
61 changes: 61 additions & 0 deletions test/unit/rules/no-builtin-form-components-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import generateRuleTests from '../../helpers/rule-test-harness.js';

generateRuleTests({
name: 'no-builtin-form-components',

config: true,

good: [
'<input type="text" />',
'<input type="checkbox" />',
'<input type="radio" />',
'<textarea></textarea>',
],

bad: [
{
template: '<Input />',
verifyResults(results) {
expect({ results }).toMatchInlineSnapshot(`
{
"results": [
{
"column": 0,
"endColumn": 9,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "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.",
"rule": "no-builtin-form-components",
"severity": 2,
"source": "<Input />",
},
],
}
`);
},
},
{
template: '<Textarea></Textarea>',
verifyResults(results) {
expect({ results }).toMatchInlineSnapshot(`
{
"results": [
{
"column": 0,
"endColumn": 21,
"endLine": 1,
"filePath": "layout.hbs",
"line": 1,
"message": "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.",
"rule": "no-builtin-form-components",
"severity": 2,
"source": "<Textarea></Textarea>",
},
],
}
`);
},
},
],
});

0 comments on commit 312ddee

Please sign in to comment.