Skip to content

Add no-builtin-form-components rule #2282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| :------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
| [no-attrs-in-components](docs/rules/no-attrs-in-components.md) | disallow usage of `this.attrs` in components | ✅ | | |
| [no-attrs-snapshot](docs/rules/no-attrs-snapshot.md) | disallow use of attrs snapshot in the `didReceiveAttrs` and `didUpdateAttrs` component hooks | ✅ | | |
| [no-builtin-form-components](docs/rules/no-builtin-form-components.md) | disallow usage of built-in form components | ✅ | | |
| [no-classic-components](docs/rules/no-classic-components.md) | enforce using Glimmer components | ✅ | | |
| [no-component-lifecycle-hooks](docs/rules/no-component-lifecycle-hooks.md) | disallow usage of "classic" ember component lifecycle hooks. Render modifiers or custom functional modifiers should be used instead. | ✅ | | |
| [no-on-calls-in-components](docs/rules/no-on-calls-in-components.md) | disallow usage of `on` to call lifecycle hooks in components | ✅ | | |
Expand Down
95 changes: 95 additions & 0 deletions docs/rules/no-builtin-form-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# ember/no-builtin-form-components

💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/ember-cli/eslint-plugin-ember#-configurations).

<!-- end auto-generated rule header -->

This rule disallows the use of Ember's built-in form components (`Input` and `Textarea`) from `@ember/component` and encourages using native HTML elements instead.

## Rule Details

Ember's built-in form components (`Input` and `Textarea`) were designed to bridge the gap between classic HTML form elements and Ember's component system. However, as Ember has evolved, using native HTML elements with modifiers has become the preferred approach for several reasons:

- Native HTML elements have better accessibility support
- They provide a more consistent developer experience with standard web development
- They have better performance characteristics
- They avoid the extra abstraction layer that the built-in components provide

This rule helps identify where these built-in form components are being used so they can be replaced with native HTML elements.

## Examples

Examples of **incorrect** code for this rule:

```js
import { Input } from '@ember/component';
```

```js
import { Textarea } from '@ember/component';
```

```js
import { Input as EmberInput, Textarea as EmberTextarea } from '@ember/component';
```

Examples of **correct** code for this rule:

```hbs
<!-- Instead of using the Input component -->
<input
value={{this.value}}
{{on "input" this.updateValue}}
/>

<!-- Instead of using the Textarea component -->
<textarea
value={{this.value}}
{{on "input" this.updateValue}}
/>
```

## Migration

### Input Component

Replace:

```hbs
<Input @value={{this.value}} @type="text" @placeholder="Enter text" {{on "input" this.handleInput}} />
```

With:

```hbs
<input
value={{this.value}}
type="text"
placeholder="Enter text"
{{on "input" this.handleInput}}
/>
```

### Textarea Component

Replace:

```hbs
<Textarea @value={{this.value}} @placeholder="Enter text" {{on "input" this.handleInput}} />
```

With:

```hbs
<textarea
value={{this.value}}
placeholder="Enter text"
{{on "input" this.handleInput}}
/>
```

## References

- [Ember Input Component API](https://api.emberjs.com/ember/release/classes/Input)
- [Ember Textarea Component API](https://api.emberjs.com/ember/release/classes/Textarea)
- [Ember Octane Modifier RFC](https://emberjs.github.io/rfcs/0373-element-modifiers.html)
1 change: 1 addition & 0 deletions lib/recommended-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
"ember/no-at-ember-render-modifiers": "error",
"ember/no-attrs-in-components": "error",
"ember/no-attrs-snapshot": "error",
"ember/no-builtin-form-components": "error",
"ember/no-capital-letters-in-routes": "error",
"ember/no-classic-classes": "error",
"ember/no-classic-components": "error",
Expand Down
48 changes: 48 additions & 0 deletions lib/rules/no-builtin-form-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

const { getImportIdentifier } = require('../utils/import');

const ERROR_MESSAGE = 'Do not use built-in form components. Use native HTML elements instead.';
const DISALLOWED_IMPORTS = new Set(['Input', 'Textarea']);

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow usage of built-in form components',
category: 'Components',
recommended: true,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-builtin-form-components.md',
},
fixable: null,
schema: [],
},

ERROR_MESSAGE,

create(context) {
const report = function (node) {
context.report({ node, message: ERROR_MESSAGE });
};

return {
ImportDeclaration(node) {
if (node.source.value === '@ember/component') {
// Check for named imports like: import { Input } from '@ember/component';
const namedImports = node.specifiers.filter(
(specifier) =>
specifier.type === 'ImportSpecifier' &&
DISALLOWED_IMPORTS.has(specifier.imported.name)
);

if (namedImports.length > 0) {
for (const specifier of namedImports) {
report(specifier);
}
}
}
},
};
},
};
88 changes: 1 addition & 87 deletions tests/__snapshots__/recommended.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -27,93 +27,7 @@ exports[`recommended rules > has the right list 1`] = `
"no-at-ember-render-modifiers",
"no-attrs-in-components",
"no-attrs-snapshot",
"no-capital-letters-in-routes",
"no-classic-classes",
"no-classic-components",
"no-component-lifecycle-hooks",
"no-computed-properties-in-native-classes",
"no-controller-access-in-routes",
"no-deeply-nested-dependent-keys-with-each",
"no-deprecated-router-transition-methods",
"no-duplicate-dependent-keys",
"no-ember-super-in-es-classes",
"no-ember-testing-in-module-scope",
"no-empty-glimmer-component-classes",
"no-function-prototype-extensions",
"no-get-with-default",
"no-get",
"no-global-jquery",
"no-implicit-injections",
"no-incorrect-calls-with-inline-anonymous-functions",
"no-incorrect-computed-macros",
"no-invalid-debug-function-arguments",
"no-invalid-dependent-keys",
"no-invalid-test-waiters",
"no-jquery",
"no-legacy-test-waiters",
"no-mixins",
"no-new-mixins",
"no-noop-setup-on-error-in-before",
"no-observers",
"no-old-shims",
"no-on-calls-in-components",
"no-pause-test",
"no-private-routing-service",
"no-restricted-resolver-tests",
"no-runloop",
"no-settled-after-test-helper",
"no-shadow-route-definition",
"no-side-effects",
"no-string-prototype-extensions",
"no-test-and-then",
"no-test-import-export",
"no-test-module-for",
"no-test-support-import",
"no-test-this-render",
"no-tracked-properties-from-args",
"no-try-invoke",
"no-unnecessary-route-path-option",
"no-volatile-computed-properties",
"prefer-ember-test-helpers",
"require-computed-macros",
"require-computed-property-dependencies",
"require-return-from-computed",
"require-super-in-lifecycle-hooks",
"require-tagless-components",
"require-valid-css-selector-in-test-helpers",
"routes-segments-snake-case",
"use-brace-expansion",
"use-ember-data-rfc-395-imports",
]
`;

exports[`recommended rules gjs config has the right list 1`] = `
[
"template-no-let-reference",
]
`;

exports[`recommended rules gts config has the right list 1`] = `
[
"template-no-let-reference",
]
`;

exports[`recommended rules has the right list 1`] = `
[
"avoid-leaking-state-in-ember-objects",
"avoid-using-needs-in-controllers",
"classic-decorator-hooks",
"classic-decorator-no-classic-methods",
"closure-actions",
"jquery-ember-run",
"new-module-imports",
"no-actions-hash",
"no-arrow-function-computed-properties",
"no-assignment-of-untracked-properties-used-in-tracking-contexts",
"no-at-ember-render-modifiers",
"no-attrs-in-components",
"no-attrs-snapshot",
"no-builtin-form-components",
"no-capital-letters-in-routes",
"no-classic-classes",
"no-classic-components",
Expand Down
86 changes: 86 additions & 0 deletions tests/lib/rules/no-builtin-form-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require('../../../lib/rules/no-builtin-form-components');
const RuleTester = require('eslint').RuleTester;

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const { ERROR_MESSAGE } = rule;
const parserOptions = {
ecmaVersion: 2022,
sourceType: 'module',
};
const ruleTester = new RuleTester({ parserOptions });

ruleTester.run('no-builtin-form-components', rule, {
valid: [
"import Component from '@ember/component';",
"import { setComponentTemplate } from '@ember/component';",
"import { helper } from '@ember/component/helper';",
"import { Input } from 'custom-component';",
"import { Textarea } from 'custom-component';",
],

invalid: [
{
code: "import { Input } from '@ember/component';",
output: null,
errors: [
{
message: ERROR_MESSAGE,
type: 'ImportSpecifier',
},
],
},
{
code: "import { Textarea } from '@ember/component';",
output: null,
errors: [
{
message: ERROR_MESSAGE,
type: 'ImportSpecifier',
},
],
},
{
code: "import { Input, Textarea } from '@ember/component';",
output: null,
errors: [
{
message: ERROR_MESSAGE,
type: 'ImportSpecifier',
},
{
message: ERROR_MESSAGE,
type: 'ImportSpecifier',
},
],
},
{
code: "import { Input as EmberInput } from '@ember/component';",
output: null,
errors: [
{
message: ERROR_MESSAGE,
type: 'ImportSpecifier',
},
],
},
{
code: "import { Textarea as EmberTextarea } from '@ember/component';",
output: null,
errors: [
{
message: ERROR_MESSAGE,
type: 'ImportSpecifier',
},
],
},
],
});