Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions .changeset/social-ghosts-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-vue': minor
---

[vue/attributes-order](https://eslint.vuejs.org/rules/attributes-order.html) can now sort attributes by line length within groups.
76 changes: 75 additions & 1 deletion docs/rules/attributes-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ Note that `v-bind="object"` syntax is considered to be the same as the next or p
"EVENTS",
"CONTENT"
],
"alphabetical": false
"alphabetical": false,
"sortLineLength": false
}]
}
```
Expand Down Expand Up @@ -201,6 +202,79 @@ Note that `v-bind="object"` syntax is considered to be the same as the next or p

</eslint-code-block>

### `"sortLineLength": true`

<eslint-code-block fix :rules="{'vue/attributes-order': ['error', {sortLineLength: true}]}">

```vue
<template>
<!-- ✓ GOOD -->
<div
a="short"
abc="value"
a-prop="longer"
boolean-prop
:my-prop="value"
very-long-prop="value"
@blur="functionCall"
@change="functionCall"
@input="handleInput">
</div>

<!-- ✗ BAD -->
<div
very-long-prop="value"
a="short"
a-prop="longer">
</div>

<div
@input="handleInput"
@blur="short">
</div>

<div
:my-prop="value"
:a="short">
</div>

</template>
```

</eslint-code-block>

### `"alphabetical": true` with `"sortLineLength": true`

When `alphabetical` and `sortLineLength` are both set to `true`, attributes within the same group are sorted primarily by their line length, and then alphabetically as a tie-breaker for attributes with the same length. This provides a clean, predictable attribute order that enhances readability.

<eslint-code-block fix :rules="{'vue/attributes-order': ['error', {alphabetical: true, sortLineLength: true}]}">

```vue
<template>
<!-- ✓ GOOD -->
<div
a="1"
b="2"
cc="3"
dd="4"
@keyup="fn"
@submit="fn"
></div>

<!-- ✗ BAD -->
<div
b="2"
a="1"
@submit="fn"
@keyup="fn"
dd="4"
cc="3"
></div>
</template>
```

</eslint-code-block>

### Custom orders

#### `['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'TWO_WAY_BINDING', 'DEFINITION', 'OTHER_DIRECTIVES', 'OTHER_ATTR', 'EVENTS', 'CONTENT']`
Expand Down
24 changes: 21 additions & 3 deletions lib/rules/attributes-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ function create(context) {
const alphabetical = Boolean(
context.options[0] && context.options[0].alphabetical
)
const sortLineLength = Boolean(
context.options[0] && context.options[0].sortLineLength
)

/** @type { { [key: string]: number } } */
const attributePosition = {}
Expand Down Expand Up @@ -347,9 +350,23 @@ function create(context) {
const { attr, position } = attributeAndPositions[index]

let valid = previousPosition <= position
if (valid && alphabetical && previousPosition === position) {
valid = isAlphabetical(previousNode, attr, sourceCode)
if (valid && previousPosition === position) {
let sortedByLength = false
if (sortLineLength) {
const prevText = sourceCode.getText(previousNode)
const currText = sourceCode.getText(attr)

if (prevText.length !== currText.length) {
valid = prevText.length < currText.length
sortedByLength = true
}
}

if (alphabetical && !sortedByLength) {
valid = isAlphabetical(previousNode, attr, sourceCode)
}
}

if (valid) {
previousNode = attr
previousPosition = position
Expand Down Expand Up @@ -450,7 +467,8 @@ module.exports = {
uniqueItems: true,
additionalItems: false
},
alphabetical: { type: 'boolean' }
alphabetical: { type: 'boolean' },
sortLineLength: { type: 'boolean' }
},
additionalProperties: false
}
Expand Down
242 changes: 242 additions & 0 deletions tests/lib/rules/attributes-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,71 @@ tester.run('attributes-order', rule, {
alphabetical: false
}
]
},
{
filename: 'test.vue',
code: `
<template>
<div
short="val"
medium-attr="value"
very-long-attribute-name="value">
</div>
</template>`,
options: [{ sortLineLength: true }]
},
{
filename: 'test.vue',
code: `
<template>
<div
v-if="condition"
v-show="show"
short="val"
medium-attr="value"
very-long-attribute-name="value">
</div>
</template>`,
options: [{ sortLineLength: false }]
},
{
filename: 'test.vue',
code: `
<template>
<div
@click="fn"
@input="handleInput"
@mouseover="handleMouseover">
</div>
</template>`,
options: [{ sortLineLength: true }]
},
{
filename: 'test.vue',
code: `
<template>
<div
:a="value"
:ab="value"
:abc="value">
</div>
</template>`,
options: [{ sortLineLength: true }]
},
{
filename: 'test.vue',
code: `
<template>
<div
bb="v"
zz="v"
aaa="v"
@click="fn"
@keyup="fn"
@submit="fn"
></div>
</template>`,
options: [{ sortLineLength: true, alphabetical: true }]
}
],

Expand Down Expand Up @@ -2102,6 +2167,183 @@ tester.run('attributes-order', rule, {
endColumn: 26
}
]
},
{
filename: 'test.vue',
code: `
<template>
<div
medium-attr="value"
short="val"
very-long-attribute-name="value">
</div>
</template>`,
output: `
<template>
<div
short="val"
medium-attr="value"
very-long-attribute-name="value">
</div>
</template>`,
options: [{ sortLineLength: true }],
errors: ['Attribute "short" should go before "medium-attr".']
},
{
filename: 'test.vue',
code: `
<template>
<div
very-long-attribute-name="value"
short="val">
</div>
</template>`,
output: `
<template>
<div
short="val"
very-long-attribute-name="value">
</div>
</template>`,
options: [{ sortLineLength: true }],
errors: ['Attribute "short" should go before "very-long-attribute-name".']
},
{
filename: 'test.vue',
code: `
<template>
<div
id="id"
v-model="foo"
very-long-attribute-name="value"
short="val">
</div>
</template>`,
output: `
<template>
<div
id="id"
v-model="foo"
short="val"
very-long-attribute-name="value">
</div>
</template>`,
options: [{ sortLineLength: true }],
errors: ['Attribute "short" should go before "very-long-attribute-name".']
},
{
filename: 'test.vue',
code: `
<template>
<div
@mouseover="handleMouseover"
@click="fn"
@input="handleInput">
</div>
</template>`,
output: `
<template>
<div
@click="fn"
@mouseover="handleMouseover"
@input="handleInput">
</div>
</template>`,
options: [{ sortLineLength: true }],
errors: [
'Attribute "@click" should go before "@mouseover".',
'Attribute "@input" should go before "@mouseover".'
]
},
{
filename: 'test.vue',
code: `
<template>
<div
:abc="value"
:a="value"
:ab="value">
</div>
</template>`,
output: `
<template>
<div
:a="value"
:abc="value"
:ab="value">
</div>
</template>`,
options: [{ sortLineLength: true }],
errors: [
'Attribute ":a" should go before ":abc".',
'Attribute ":ab" should go before ":abc".'
]
},
{
filename: 'test.vue',
code: `
<template>
<div
very-long-binding="longerValue"
short="obj"
@click="fn">
</div>
</template>`,
output: `
<template>
<div
short="obj"
very-long-binding="longerValue"
@click="fn">
</div>
</template>`,
options: [{ sortLineLength: true }],
errors: ['Attribute "short" should go before "very-long-binding".']
},
{
filename: 'test.vue',
code: `
<template>
<div
aa="v"
z="v"
></div>
</template>`,
output: `
<template>
<div
z="v"
aa="v"
></div>
</template>`,
options: [{ sortLineLength: true, alphabetical: true }],
errors: [{ message: 'Attribute "z" should go before "aa".' }]
},
{
filename: 'test.vue',
code: `
<template>
<div
zz="v"
bb="v"
@keyup="fn"
@click="fn"
></div>
</template>`,
output: `
<template>
<div
bb="v"
zz="v"
@click="fn"
@keyup="fn"
></div>
</template>`,
options: [{ sortLineLength: true, alphabetical: true }],
errors: [
{ message: 'Attribute "bb" should go before "zz".' },
{ message: 'Attribute "@click" should go before "@keyup".' }
]
}
]
})