Skip to content

Commit b66bb57

Browse files
committed
Extract rule: template-no-jsx-attributes
1 parent 3f6a7c8 commit b66bb57

4 files changed

Lines changed: 359 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,12 @@ rules in templates can be disabled with eslint directives with mustache or html
322322
| [no-runloop](docs/rules/no-runloop.md) | disallow usage of `@ember/runloop` functions || | |
323323
| [require-fetch-import](docs/rules/require-fetch-import.md) | enforce explicit import for `fetch()` | | | |
324324

325+
### Possible Errors
326+
327+
| Name | Description | 💼 | 🔧 | 💡 |
328+
| :--------------------------------------------------------------------- | :-------------------------------------- | :- | :- | :- |
329+
| [template-no-jsx-attributes](docs/rules/template-no-jsx-attributes.md) | disallow JSX-style camelCase attributes | | 🔧 | |
330+
325331
### Routes
326332

327333
| Name                             | Description | 💼 | 🔧 | 💡 |
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# ember/template-no-jsx-attributes
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Disallows JSX-style camelCase attributes in templates.
8+
9+
Folks coming from React may have developed habits around how they type attributes on elements.
10+
JSX isn't HTML (it's JS), so in JS, you can't have kebab-case identifiers, so JSX uses camelCase.
11+
12+
However, since Ember uses HTML, camelCase attributes are not valid when writing components.
13+
14+
## Examples
15+
16+
This rule **forbids** the following attributes:
17+
18+
- acceptCharset
19+
- accessKey
20+
- allowFullScreen
21+
- allowTransparency
22+
- autoComplete
23+
- autoFocus
24+
- autoPlay
25+
- cellPadding
26+
- cellSpacing
27+
- charSet
28+
- className
29+
- contentEditable
30+
- contextMenu
31+
- crossOrigin
32+
- dataTime
33+
- encType
34+
- formAction
35+
- formEncType
36+
- formMethod
37+
- formNoValidate
38+
- formTarget
39+
- frameBorder
40+
- httpEquiv
41+
- inputMode
42+
- keyParams
43+
- keyType
44+
- noValidate
45+
- marginHeight
46+
- marginWidth
47+
- maxLength
48+
- mediaGroup
49+
- minLength
50+
- radioGroup
51+
- readOnly
52+
- rowSpan
53+
- spellCheck
54+
- srcDoc
55+
- srcSet
56+
- tabIndex
57+
- useMap
58+
59+
This rule **forbids** the following:
60+
61+
```gjs
62+
<template>
63+
<div className='foo'></div>
64+
<div contentEditable='true'></div>
65+
<img srcSet='image.jpg 1x, image@2x.jpg 2x' />
66+
</template>
67+
```
68+
69+
This rule **allows** the following:
70+
71+
```gjs
72+
<template>
73+
<div class='foo'></div>
74+
<div contenteditable='true'></div>
75+
<img srcset='image.jpg 1x, image@2x.jpg 2x' />
76+
</template>
77+
```
78+
79+
## Migration
80+
81+
Convert attributes to kebab-case[^camelCaseNote]
82+
83+
- `<div className="...">` -> `<div class="...">`
84+
- `<video autoPlay>` -> `<video auto-play>`
85+
- `<div contentEditable>` -> `<div content-editable>`
86+
- etc
87+
88+
[^camelCaseNote]: keep in mind that `@args`, and `<:blocks>` should be js-compatible identifiers and be camelCase
89+
90+
## References
91+
92+
- [HTML Attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes)
93+
- [React JSX differences](https://reactjs.org/docs/dom-elements.html#differences-in-attributes)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const fixMap = {
2+
acceptCharset: 'accept-charset',
3+
srcSet: 'srcset',
4+
accessKey: 'accesskey',
5+
allowFullScreen: 'allowfullscreen',
6+
allowTransparency: 'allowtransparency',
7+
autoComplete: 'autocomplete',
8+
autoFocus: 'autofocus',
9+
autoPlay: 'autoplay',
10+
cellPadding: 'cellpadding',
11+
cellSpacing: 'cellspacing',
12+
charSet: 'charset',
13+
className: 'class',
14+
contentEditable: 'contenteditable',
15+
contextMenu: 'contextmenu',
16+
crossOrigin: 'crossorigin',
17+
dateTime: 'datetime',
18+
encType: 'enctype',
19+
formAction: 'formaction',
20+
formEncType: 'formenctype',
21+
formMethod: 'formmethod',
22+
formNoValidate: 'formnovalidate',
23+
formTarget: 'formtarget',
24+
frameBorder: 'frameborder',
25+
httpEquiv: 'http-equiv',
26+
inputMode: 'inputmode',
27+
keyType: 'keytype',
28+
noValidate: 'novalidate',
29+
marginHeight: 'marginheight',
30+
marginWidth: 'marginwidth',
31+
maxLength: 'maxlength',
32+
minLength: 'minlength',
33+
radioGroup: 'radiogroup',
34+
readOnly: 'readonly',
35+
rowSpan: 'rowspan',
36+
colSpan: 'colspan',
37+
spellCheck: 'spellcheck',
38+
srcDoc: 'srcdoc',
39+
tabIndex: 'tabindex',
40+
useMap: 'usemap',
41+
};
42+
43+
const camelCaseAttributes = Object.keys(fixMap);
44+
45+
function getMessage(name) {
46+
if (name === 'className') {
47+
return `Attribute, ${name}, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.`;
48+
}
49+
50+
return `Incorrect html attribute name detected - "${name}", is probably unintended. Attributes in HTML are kebeb case.`;
51+
}
52+
53+
/** @type {import('eslint').Rule.RuleModule} */
54+
module.exports = {
55+
meta: {
56+
type: 'problem',
57+
docs: {
58+
description: 'disallow JSX-style camelCase attributes',
59+
category: 'Possible Errors',
60+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-jsx-attributes.md',
61+
templateMode: 'both',
62+
},
63+
fixable: 'code',
64+
schema: [],
65+
messages: {},
66+
originallyFrom: {
67+
name: 'ember-template-lint',
68+
rule: 'lib/rules/no-jsx-attributes.js',
69+
docs: 'docs/rule/no-jsx-attributes.md',
70+
tests: 'test/unit/rules/no-jsx-attributes-test.js',
71+
},
72+
},
73+
74+
create(context) {
75+
return {
76+
GlimmerAttrNode(node) {
77+
const key = node.name;
78+
const isJSXProbably = camelCaseAttributes.includes(key);
79+
80+
if (!isJSXProbably) {
81+
return;
82+
}
83+
84+
context.report({
85+
node,
86+
message: getMessage(key),
87+
fix: fixMap[key]
88+
? (fixer) => {
89+
const sourceCode = context.getSourceCode();
90+
const text = sourceCode.getText(node);
91+
const valueMatch = text.match(/^[^=]+(=.*)?$/);
92+
const value = valueMatch && valueMatch[1] ? valueMatch[1] : '';
93+
return fixer.replaceText(node, `${fixMap[key]}${value}`);
94+
}
95+
: null,
96+
});
97+
},
98+
};
99+
},
100+
};
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
const rule = require('../../../lib/rules/template-no-jsx-attributes');
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-jsx-attributes', rule, {
10+
valid: [
11+
'<template><div></div></template>',
12+
'<template><div class="foo"></div></template>',
13+
'<template><div class></div></template>',
14+
'<template><div autoplay></div></template>',
15+
'<template><div contenteditable="true"></div></template>',
16+
],
17+
invalid: [
18+
{
19+
code: '<template><div acceptCharset="utf-8"></div></template>',
20+
output: '<template><div accept-charset="utf-8"></div></template>',
21+
errors: [
22+
{
23+
message:
24+
'Incorrect html attribute name detected - "acceptCharset", is probably unintended. Attributes in HTML are kebeb case.',
25+
},
26+
],
27+
},
28+
{
29+
code: '<template><div contentEditable="true"></div></template>',
30+
output: '<template><div contenteditable="true"></div></template>',
31+
errors: [
32+
{
33+
message:
34+
'Incorrect html attribute name detected - "contentEditable", is probably unintended. Attributes in HTML are kebeb case.',
35+
},
36+
],
37+
},
38+
{
39+
code: '<template><div className></div></template>',
40+
output: '<template><div class></div></template>',
41+
errors: [
42+
{
43+
message:
44+
"Attribute, className, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.",
45+
},
46+
],
47+
},
48+
{
49+
code: '<template><div className="foo"></div></template>',
50+
output: '<template><div class="foo"></div></template>',
51+
errors: [
52+
{
53+
message:
54+
"Attribute, className, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.",
55+
},
56+
],
57+
},
58+
59+
{
60+
code: '<template><div autoPlay></div></template>',
61+
output: '<template><div autoplay></div></template>',
62+
errors: [
63+
{
64+
message:
65+
'Incorrect html attribute name detected - "autoPlay", is probably unintended. Attributes in HTML are kebeb case.',
66+
},
67+
],
68+
},
69+
{
70+
code: '<template><div contentEditable></div></template>',
71+
output: '<template><div contenteditable></div></template>',
72+
errors: [
73+
{
74+
message:
75+
'Incorrect html attribute name detected - "contentEditable", is probably unintended. Attributes in HTML are kebeb case.',
76+
},
77+
],
78+
},
79+
],
80+
});
81+
82+
const hbsRuleTester = new RuleTester({
83+
parser: require.resolve('ember-eslint-parser/hbs'),
84+
parserOptions: {
85+
ecmaVersion: 2022,
86+
sourceType: 'module',
87+
},
88+
});
89+
90+
hbsRuleTester.run('template-no-jsx-attributes', rule, {
91+
valid: [
92+
'<div></div>',
93+
'<div class="foo"></div>',
94+
'<div class></div>',
95+
'<div autoplay></div>',
96+
'<div contenteditable="true"></div>',
97+
],
98+
invalid: [
99+
{
100+
code: '<div acceptCharset="utf-8"></div>',
101+
output: '<div accept-charset="utf-8"></div>',
102+
errors: [
103+
{
104+
message:
105+
'Incorrect html attribute name detected - "acceptCharset", is probably unintended. Attributes in HTML are kebeb case.',
106+
},
107+
],
108+
},
109+
{
110+
code: '<div contentEditable="true"></div>',
111+
output: '<div contenteditable="true"></div>',
112+
errors: [
113+
{
114+
message:
115+
'Incorrect html attribute name detected - "contentEditable", is probably unintended. Attributes in HTML are kebeb case.',
116+
},
117+
],
118+
},
119+
{
120+
code: '<div className></div>',
121+
output: '<div class></div>',
122+
errors: [
123+
{
124+
message:
125+
"Attribute, className, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.",
126+
},
127+
],
128+
},
129+
{
130+
code: '<div className="foo"></div>',
131+
output: '<div class="foo"></div>',
132+
errors: [
133+
{
134+
message:
135+
"Attribute, className, does not assign the 'class' attribute as it would in JSX. To assign the 'class' attribute, set the 'class' attribute, instead of 'className'. In HTML, all attributes are valid, but 'className' doesn't do anything.",
136+
},
137+
],
138+
},
139+
{
140+
code: '<div autoPlay></div>',
141+
output: '<div autoplay></div>',
142+
errors: [
143+
{
144+
message:
145+
'Incorrect html attribute name detected - "autoPlay", is probably unintended. Attributes in HTML are kebeb case.',
146+
},
147+
],
148+
},
149+
{
150+
code: '<div contentEditable></div>',
151+
output: '<div contenteditable></div>',
152+
errors: [
153+
{
154+
message:
155+
'Incorrect html attribute name detected - "contentEditable", is probably unintended. Attributes in HTML are kebeb case.',
156+
},
157+
],
158+
},
159+
],
160+
});

0 commit comments

Comments
 (0)