Skip to content

Commit caf3127

Browse files
committed
Extract rule: template-attribute-order
1 parent 5d2ad94 commit caf3127

3 files changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# ember/template-attribute-order
2+
3+
💼 This rule is enabled in the following [configs](https://github.com/ember-cli/eslint-plugin-ember#-configurations): `strict-gjs`, `strict-gts`.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Enforces a consistent ordering of attributes in template elements. This helps improve readability and maintainability of templates.
8+
9+
## Rule Details
10+
11+
This rule enforces a consistent order for attributes on template elements. By default, it follows this order:
12+
13+
1. `class`
14+
2. `id`
15+
3. `role`
16+
4. `aria-*` attributes
17+
5. `data-test-*` attributes
18+
6. `type`
19+
7. `name`
20+
8. `value`
21+
9. `placeholder`
22+
10. `disabled`
23+
24+
## Examples
25+
26+
Examples of **incorrect** code for this rule:
27+
28+
```gjs
29+
<template>
30+
<div id="main" class="container"></div>
31+
</template>
32+
```
33+
34+
```gjs
35+
<template>
36+
<button aria-label="Submit" role="button">Send</button>
37+
</template>
38+
```
39+
40+
Examples of **correct** code for this rule:
41+
42+
```gjs
43+
<template>
44+
<div class="container" id="main"></div>
45+
</template>
46+
```
47+
48+
```gjs
49+
<template>
50+
<button class="btn" role="button" aria-label="Submit">Send</button>
51+
</template>
52+
```
53+
54+
## Configuration
55+
56+
You can customize the order by providing an `order` array:
57+
58+
```js
59+
module.exports = {
60+
rules: {
61+
'ember/template-attribute-order': [
62+
'error',
63+
{
64+
order: ['class', 'id', 'role', 'aria-', 'type'],
65+
},
66+
],
67+
},
68+
};
69+
```
70+
71+
## References
72+
73+
- [ember-template-lint attribute-order](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/attribute-order.md)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'enforce consistent ordering of attributes in template elements',
7+
category: 'Stylistic Issues',
8+
strictGjs: true,
9+
strictGts: true,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-attribute-order.md',
11+
},
12+
fixable: null,
13+
schema: [
14+
{
15+
type: 'object',
16+
properties: {
17+
order: {
18+
type: 'array',
19+
items: {
20+
type: 'string',
21+
},
22+
},
23+
},
24+
additionalProperties: false,
25+
},
26+
],
27+
messages: {
28+
wrongOrder: 'Attribute "{{currentAttr}}" should come {{position}} "{{expectedAttr}}".',
29+
},
30+
},
31+
32+
create(context) {
33+
const options = context.options[0] || {};
34+
const order = options.order || [
35+
'class',
36+
'id',
37+
'role',
38+
'aria-',
39+
'data-test-',
40+
'type',
41+
'name',
42+
'value',
43+
'placeholder',
44+
'disabled',
45+
];
46+
47+
function getAttributeCategory(attrName) {
48+
for (const category of order) {
49+
if (category.endsWith('-')) {
50+
if (attrName.startsWith(category)) {
51+
return category;
52+
}
53+
} else if (attrName === category) {
54+
return category;
55+
}
56+
}
57+
return null;
58+
}
59+
60+
function getExpectedIndex(attrName) {
61+
const category = getAttributeCategory(attrName);
62+
if (category === null) {
63+
return order.length; // Unknown attributes go last
64+
}
65+
return order.indexOf(category);
66+
}
67+
68+
return {
69+
GlimmerElementNode(node) {
70+
if (!node.attributes || node.attributes.length < 2) {
71+
return;
72+
}
73+
74+
const attributes = node.attributes.filter(
75+
(attr) => attr.type === 'GlimmerAttrNode' && attr.name
76+
);
77+
78+
for (let i = 1; i < attributes.length; i++) {
79+
const current = attributes[i];
80+
const currentIndex = getExpectedIndex(current.name);
81+
82+
for (let j = 0; j < i; j++) {
83+
const previous = attributes[j];
84+
const previousIndex = getExpectedIndex(previous.name);
85+
86+
if (currentIndex < previousIndex) {
87+
context.report({
88+
node: current,
89+
messageId: 'wrongOrder',
90+
data: {
91+
currentAttr: current.name,
92+
position: 'before',
93+
expectedAttr: previous.name,
94+
},
95+
});
96+
break;
97+
}
98+
}
99+
}
100+
},
101+
};
102+
},
103+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const rule = require('../../../lib/rules/template-attribute-order');
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-attribute-order', rule, {
10+
valid: [
11+
'<template><div class="foo" id="bar"></div></template>',
12+
'<template><button class="btn" role="button" aria-label="Submit"></button></template>',
13+
'<template><input type="text" name="username" value=""></template>',
14+
'<template><div data-test-id="foo"></div></template>',
15+
],
16+
17+
invalid: [
18+
{
19+
code: '<template><div id="bar" class="foo"></div></template>',
20+
output: null,
21+
errors: [{ messageId: 'wrongOrder' }],
22+
},
23+
{
24+
code: '<template><button aria-label="Submit" role="button"></button></template>',
25+
output: null,
26+
errors: [{ messageId: 'wrongOrder' }],
27+
},
28+
{
29+
code: '<template><input name="username" type="text"></template>',
30+
output: null,
31+
errors: [{ messageId: 'wrongOrder' }],
32+
},
33+
],
34+
});

0 commit comments

Comments
 (0)