Skip to content

Commit 0b816dc

Browse files
authored
Add new attributes order for dynamic and static props (#2068)
1 parent ad07deb commit 0b816dc

File tree

3 files changed

+293
-4
lines changed

3 files changed

+293
-4
lines changed

docs/rules/attributes-order.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ This rule aims to enforce ordering of component attributes. The default order is
3535
- `OTHER_DIRECTIVES`
3636
e.g. 'v-custom-directive'
3737
- `OTHER_ATTR`
38-
e.g. 'custom-prop="foo"', 'v-bind:prop="foo"', ':prop="foo"'
38+
alias for `[ATTR_DYNAMIC, ATTR_STATIC, ATTR_SHORTHAND_BOOL]`:
39+
- `ATTR_DYNAMIC`
40+
e.g. 'v-bind:prop="foo"', ':prop="foo"'
41+
- `ATTR_STATIC`
42+
e.g. 'prop="foo"', 'custom-prop="foo"'
43+
- `ATTR_SHORTHAND_BOOL`
44+
e.g. 'boolean-prop'
3945
- `EVENTS`
4046
e.g. '@click="functionCall"', 'v-on="event"'
4147
- `CONTENT`

lib/rules/attributes-order.js

+50-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const ATTRS = {
2020
TWO_WAY_BINDING: 'TWO_WAY_BINDING',
2121
OTHER_DIRECTIVES: 'OTHER_DIRECTIVES',
2222
OTHER_ATTR: 'OTHER_ATTR',
23+
ATTR_STATIC: 'ATTR_STATIC',
24+
ATTR_DYNAMIC: 'ATTR_DYNAMIC',
25+
ATTR_SHORTHAND_BOOL: 'ATTR_SHORTHAND_BOOL',
2326
EVENTS: 'EVENTS',
2427
CONTENT: 'CONTENT'
2528
}
@@ -66,6 +69,15 @@ function isVBindObject(node) {
6669
return isVBind(node) && node.key.argument == null
6770
}
6871

72+
/**
73+
* Check whether the given attribute is a shorthand boolean like `selected`.
74+
* @param {VAttribute | VDirective | undefined | null} node
75+
* @returns { node is VAttribute }
76+
*/
77+
function isVShorthandBoolean(node) {
78+
return isVAttribute(node) && !node.value
79+
}
80+
6981
/**
7082
* @param {VAttribute | VDirective} attribute
7183
* @param {SourceCode} sourceCode
@@ -153,7 +165,13 @@ function getAttributeType(attribute) {
153165
case 'slot-scope':
154166
return ATTRS.SLOT
155167
default:
156-
return ATTRS.OTHER_ATTR
168+
if (isVBind(attribute)) {
169+
return ATTRS.ATTR_DYNAMIC
170+
}
171+
if (isVShorthandBoolean(attribute)) {
172+
return ATTRS.ATTR_SHORTHAND_BOOL
173+
}
174+
return ATTRS.ATTR_STATIC
157175
}
158176
}
159177

@@ -191,6 +209,11 @@ function isAlphabetical(prevNode, currNode, sourceCode) {
191209
*/
192210
function create(context) {
193211
const sourceCode = context.getSourceCode()
212+
const otherAttrs = [
213+
ATTRS.ATTR_DYNAMIC,
214+
ATTRS.ATTR_STATIC,
215+
ATTRS.ATTR_SHORTHAND_BOOL
216+
]
194217
let attributeOrder = [
195218
ATTRS.DEFINITION,
196219
ATTRS.LIST_RENDERING,
@@ -200,12 +223,36 @@ function create(context) {
200223
[ATTRS.UNIQUE, ATTRS.SLOT],
201224
ATTRS.TWO_WAY_BINDING,
202225
ATTRS.OTHER_DIRECTIVES,
203-
ATTRS.OTHER_ATTR,
226+
otherAttrs,
204227
ATTRS.EVENTS,
205228
ATTRS.CONTENT
206229
]
207230
if (context.options[0] && context.options[0].order) {
208-
attributeOrder = context.options[0].order
231+
attributeOrder = [...context.options[0].order]
232+
233+
// check if `OTHER_ATTR` is valid
234+
for (const item of attributeOrder.flat()) {
235+
if (item === ATTRS.OTHER_ATTR) {
236+
for (const attribute of attributeOrder.flat()) {
237+
if (otherAttrs.includes(attribute)) {
238+
throw new Error(
239+
`Value "${ATTRS.OTHER_ATTR}" is not allowed with "${attribute}".`
240+
)
241+
}
242+
}
243+
}
244+
}
245+
246+
// expand `OTHER_ATTR` alias
247+
for (const [index, item] of attributeOrder.entries()) {
248+
if (item === ATTRS.OTHER_ATTR) {
249+
attributeOrder[index] = otherAttrs
250+
} else if (Array.isArray(item) && item.includes(ATTRS.OTHER_ATTR)) {
251+
const attributes = item.filter((i) => i !== ATTRS.OTHER_ATTR)
252+
attributes.push(...otherAttrs)
253+
attributeOrder[index] = attributes
254+
}
255+
}
209256
}
210257
const alphabetical = Boolean(
211258
context.options[0] && context.options[0].alphabetical

tests/lib/rules/attributes-order.js

+236
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,138 @@ tester.run('attributes-order', rule, {
483483
</div>
484484
</template>`,
485485
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }]
486+
},
487+
488+
// https://github.com/vuejs/eslint-plugin-vue/issues/1728
489+
{
490+
filename: 'test.vue',
491+
code: `
492+
<template>
493+
<div
494+
:prop-bind="prop"
495+
prop-one="prop"
496+
prop-two="prop">
497+
</div>
498+
</template>`,
499+
options: [
500+
{
501+
order: ['ATTR_DYNAMIC', 'ATTR_STATIC'],
502+
alphabetical: false
503+
}
504+
]
505+
},
506+
{
507+
filename: 'test.vue',
508+
code: `
509+
<template>
510+
<div
511+
v-model="a
512+
:class="b"
513+
:is="c"
514+
prop-one="d"
515+
class="e"
516+
prop-two="f">
517+
</div>
518+
</template>`,
519+
options: [
520+
{
521+
order: ['TWO_WAY_BINDING', 'ATTR_DYNAMIC', 'ATTR_STATIC'],
522+
alphabetical: false
523+
}
524+
]
525+
},
526+
{
527+
filename: 'test.vue',
528+
code: `
529+
<template>
530+
<div
531+
prop-one="a"
532+
prop-three="b"
533+
:prop-two="c">
534+
</div>
535+
</template>`,
536+
options: [
537+
{
538+
order: [
539+
'DEFINITION',
540+
'LIST_RENDERING',
541+
'CONDITIONALS',
542+
'RENDER_MODIFIERS',
543+
'GLOBAL',
544+
['UNIQUE', 'SLOT'],
545+
'TWO_WAY_BINDING',
546+
'OTHER_DIRECTIVES',
547+
'ATTR_STATIC',
548+
'ATTR_DYNAMIC',
549+
'EVENTS',
550+
'CONTENT'
551+
],
552+
alphabetical: false
553+
}
554+
]
555+
},
556+
{
557+
filename: 'test.vue',
558+
code: `
559+
<template>
560+
<div
561+
prop-one="a"
562+
:prop-two="b"
563+
prop-three="c">
564+
</div>
565+
</template>`,
566+
options: [
567+
{
568+
order: [
569+
'DEFINITION',
570+
'LIST_RENDERING',
571+
'CONDITIONALS',
572+
'RENDER_MODIFIERS',
573+
'GLOBAL',
574+
['UNIQUE', 'SLOT'],
575+
'TWO_WAY_BINDING',
576+
'OTHER_DIRECTIVES',
577+
['ATTR_STATIC', 'ATTR_DYNAMIC'],
578+
'EVENTS',
579+
'CONTENT'
580+
],
581+
alphabetical: false
582+
}
583+
]
584+
},
585+
586+
// https://github.com/vuejs/eslint-plugin-vue/issues/1870
587+
{
588+
filename: 'test.vue',
589+
code: `
590+
<template>
591+
<div
592+
boolean-prop
593+
prop-one="a"
594+
prop-two="b"
595+
:prop-three="c">
596+
</div>
597+
</template>`,
598+
options: [
599+
{
600+
order: [
601+
'DEFINITION',
602+
'LIST_RENDERING',
603+
'CONDITIONALS',
604+
'RENDER_MODIFIERS',
605+
'GLOBAL',
606+
['UNIQUE', 'SLOT'],
607+
'TWO_WAY_BINDING',
608+
'OTHER_DIRECTIVES',
609+
'ATTR_SHORTHAND_BOOL',
610+
'ATTR_STATIC',
611+
'ATTR_DYNAMIC',
612+
'EVENTS',
613+
'CONTENT'
614+
],
615+
alphabetical: false
616+
}
617+
]
486618
}
487619
],
488620

@@ -1528,6 +1660,110 @@ tester.run('attributes-order', rule, {
15281660
attr="foo"/>
15291661
</template>`,
15301662
errors: ['Attribute "@click" should go before "v-bind".']
1663+
},
1664+
1665+
{
1666+
filename: 'test.vue',
1667+
code: `
1668+
<template>
1669+
<div
1670+
v-bind:prop-one="a"
1671+
prop-two="b"
1672+
:prop-three="c"/>
1673+
</template>`,
1674+
options: [{ order: ['ATTR_STATIC', 'ATTR_DYNAMIC'] }],
1675+
output: `
1676+
<template>
1677+
<div
1678+
prop-two="b"
1679+
v-bind:prop-one="a"
1680+
:prop-three="c"/>
1681+
</template>`,
1682+
errors: ['Attribute "prop-two" should go before "v-bind:prop-one".']
1683+
},
1684+
1685+
{
1686+
filename: 'test.vue',
1687+
code: `
1688+
<template>
1689+
<div
1690+
:prop-one="a"
1691+
v-model="value"
1692+
prop-two="b"
1693+
:prop-three="c"
1694+
class="class"
1695+
boolean-prop/>
1696+
</template>`,
1697+
options: [
1698+
{
1699+
order: [
1700+
'LIST_RENDERING',
1701+
'CONDITIONALS',
1702+
'RENDER_MODIFIERS',
1703+
'TWO_WAY_BINDING',
1704+
'OTHER_DIRECTIVES',
1705+
'ATTR_DYNAMIC',
1706+
'ATTR_STATIC',
1707+
'ATTR_SHORTHAND_BOOL',
1708+
'EVENTS'
1709+
]
1710+
}
1711+
],
1712+
output: `
1713+
<template>
1714+
<div
1715+
v-model="value"
1716+
:prop-one="a"
1717+
:prop-three="c"
1718+
prop-two="b"
1719+
class="class"
1720+
boolean-prop/>
1721+
</template>`,
1722+
errors: [
1723+
'Attribute "v-model" should go before ":prop-one".',
1724+
'Attribute ":prop-three" should go before "prop-two".'
1725+
]
1726+
},
1727+
1728+
{
1729+
filename: 'test.vue',
1730+
code: `
1731+
<template>
1732+
<div
1733+
:prop-one="a"
1734+
v-model="value"
1735+
boolean-prop
1736+
prop-two="b"
1737+
:prop-three="c"/>
1738+
</template>`,
1739+
options: [
1740+
{
1741+
order: [
1742+
'UNIQUE',
1743+
'LIST_RENDERING',
1744+
'CONDITIONALS',
1745+
'RENDER_MODIFIERS',
1746+
'GLOBAL',
1747+
'TWO_WAY_BINDING',
1748+
'OTHER_DIRECTIVES',
1749+
['ATTR_STATIC', 'ATTR_DYNAMIC', 'ATTR_SHORTHAND_BOOL'],
1750+
'EVENTS',
1751+
'CONTENT',
1752+
'DEFINITION',
1753+
'SLOT'
1754+
]
1755+
}
1756+
],
1757+
output: `
1758+
<template>
1759+
<div
1760+
v-model="value"
1761+
:prop-one="a"
1762+
boolean-prop
1763+
prop-two="b"
1764+
:prop-three="c"/>
1765+
</template>`,
1766+
errors: ['Attribute "v-model" should go before ":prop-one".']
15311767
}
15321768
]
15331769
})

0 commit comments

Comments
 (0)