Skip to content

Commit 21dfb40

Browse files
committed
feat: add prefer-use-template-ref rule
1 parent 8b877f7 commit 21dfb40

File tree

5 files changed

+378
-0
lines changed

5 files changed

+378
-0
lines changed

docs/rules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ For example:
270270
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
271271
| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
272272
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
273+
| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` over `ref` for template refs | :wrench: | :hammer: |
273274
| [vue/require-default-export](./require-default-export.md) | require components to be the default export | | :warning: |
274275
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | :hammer: |
275276
| [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |

docs/rules/prefer-use-template-ref.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/prefer-use-template-ref
5+
description: require using `useTemplateRef` over `ref` for template refs
6+
---
7+
8+
# vue/prefer-use-template-ref
9+
10+
> require using `useTemplateRef` over `ref` for template refs
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
13+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
14+
15+
## :book: Rule Details
16+
17+
Vue 3.5 introduced a new way of obtaining template refs via
18+
the [useTemplateRef()](https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs) API.
19+
20+
This rule enforces using the new `useTemplateRef` function over `ref` for template refs.
21+
22+
<eslint-code-block fix :rules="{'vue/prefer-use-template-ref': ['error']}">
23+
24+
```vue
25+
26+
<template>
27+
<div ref="divRef"></div>
28+
<button ref="submitter">Submit</button>
29+
</template>
30+
31+
<script>
32+
import { ref, useTemplateRef } from 'vue';
33+
34+
/* ✓ GOOD */
35+
const divRef = useTemplateRef('divRef');
36+
const div = useTemplateRef('divRef');
37+
const loremIpsum = useTemplateRef('divRef');
38+
const submitButton = useTemplateRef('submitter');
39+
/* ✗ BAD */
40+
const divRef = ref();
41+
const submitter = ref();
42+
</script>
43+
```
44+
45+
</eslint-code-block>
46+
47+
## :wrench: Options
48+
49+
Nothing.
50+
51+
## :mag: Implementation
52+
53+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-use-template-ref.js)
54+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-use-template-ref.js)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ const plugin = {
208208
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
209209
'prefer-template': require('./rules/prefer-template'),
210210
'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),
211+
'prefer-use-template-ref': require('./rules/prefer-use-template-ref'),
211212
'prop-name-casing': require('./rules/prop-name-casing'),
212213
'quote-props': require('./rules/quote-props'),
213214
'require-component-is': require('./rules/require-component-is'),

lib/rules/prefer-use-template-ref.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* @author Thomasan1999
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
/** @type {import("eslint").Rule.RuleModule} */
10+
module.exports = {
11+
meta: {
12+
type: 'suggestion',
13+
docs: {
14+
description:
15+
'require using `useTemplateRef` over `ref` for template refs',
16+
categories: undefined,
17+
url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html'
18+
},
19+
fixable: 'code',
20+
schema: [],
21+
messages: {
22+
preferUseTemplateRef: "Replace 'ref' with 'useTemplateRef'."
23+
}
24+
},
25+
/** @param {RuleContext} context */
26+
create(context) {
27+
/** @type Set<string> */
28+
const templateRefs = new Set()
29+
30+
/**
31+
* @typedef ScriptRef
32+
* @type {{node: Expression, ref: string}}
33+
*/
34+
35+
/**
36+
* @type ScriptRef[] */
37+
const scriptRefs = []
38+
39+
return utils.compositingVisitors(
40+
utils.defineTemplateBodyVisitor(
41+
context,
42+
{
43+
'VAttribute[directive=false]'(node) {
44+
if (node.key.name === 'ref' && node.value?.value) {
45+
templateRefs.add(node.value.value)
46+
}
47+
}
48+
},
49+
{
50+
VariableDeclarator(declarator) {
51+
// @ts-ignore
52+
if (declarator.init?.callee?.name !== 'ref') {
53+
return
54+
}
55+
56+
scriptRefs.push({
57+
node: declarator.init,
58+
// @ts-ignore
59+
ref: declarator.id.name
60+
})
61+
}
62+
}
63+
),
64+
{
65+
'Program:exit'() {
66+
for (const templateRef of templateRefs) {
67+
const scriptRef = scriptRefs.find(
68+
(scriptRef) => scriptRef.ref === templateRef
69+
)
70+
71+
if (!scriptRef) {
72+
continue
73+
}
74+
75+
context.report({
76+
node: scriptRef.node,
77+
messageId: 'preferUseTemplateRef',
78+
fix(fixer) {
79+
return fixer.replaceText(
80+
scriptRef.node,
81+
`useTemplateRef('${scriptRef.ref}')`
82+
)
83+
}
84+
})
85+
}
86+
}
87+
}
88+
)
89+
}
90+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/**
2+
* @author Thomasan1999
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('../../eslint-compat').RuleTester
8+
const rule = require('../../../lib/rules/prefer-use-template-ref')
9+
10+
const tester = new RuleTester({
11+
languageOptions: {
12+
parser: require('vue-eslint-parser'),
13+
ecmaVersion: 2020,
14+
sourceType: 'module'
15+
}
16+
})
17+
18+
tester.run('prefer-use-template-ref', rule, {
19+
valid: [
20+
{
21+
filename: 'single-use-template-ref.vue',
22+
code: `
23+
<template>
24+
<div ref="root" />
25+
</template>
26+
27+
<script>
28+
import { useTemplateRef } from 'vue';
29+
30+
const root = useTemplateRef('root');
31+
</script>
32+
`
33+
},
34+
{
35+
filename: 'multiple-use-template-refs.vue',
36+
code: `
37+
<template>
38+
<button ref="button">Content</button>
39+
<a href="" ref="link">Link</a>
40+
</template>
41+
42+
<script>
43+
import { useTemplateRef } from 'vue';
44+
45+
const buttonRef = useTemplateRef('button');
46+
const link = useTemplateRef('link');
47+
</script>
48+
`
49+
},
50+
{
51+
filename: 'use-template-ref-in-block.vue',
52+
code: `
53+
<template>
54+
<div>
55+
<ul>
56+
<li ref="firstListItem">Morning</li>
57+
<li>Afternoon</li>
58+
<li>Evening</li>
59+
</ul>
60+
</div>
61+
</template>
62+
63+
<script>
64+
import { useTemplateRef } from 'vue';
65+
66+
function getFirstListItemElement() {
67+
const firstListItem = useTemplateRef('firstListItem');
68+
}
69+
</script>
70+
`
71+
}
72+
],
73+
invalid: [
74+
{
75+
filename: 'single-ref.vue',
76+
code: `
77+
<template>
78+
<div ref="root"/>
79+
</template>
80+
81+
<script>
82+
import { ref } from 'vue';
83+
84+
const root = ref();
85+
</script>
86+
`,
87+
output: `
88+
<template>
89+
<div ref="root"/>
90+
</template>
91+
92+
<script>
93+
import { ref } from 'vue';
94+
95+
const root = useTemplateRef('root');
96+
</script>
97+
`,
98+
errors: [
99+
{
100+
messageId: 'preferUseTemplateRef',
101+
line: 9,
102+
column: 22
103+
}
104+
]
105+
},
106+
{
107+
filename: 'one-ref-unused-in-script.vue',
108+
code: `
109+
<template>
110+
<button ref="button">Content</button>
111+
<a href="" ref="link">Link</a>
112+
</template>
113+
114+
<script>
115+
import { ref } from 'vue';
116+
117+
const buttonRef = ref();
118+
const link = ref();
119+
</script>
120+
`,
121+
output: `
122+
<template>
123+
<button ref="button">Content</button>
124+
<a href="" ref="link">Link</a>
125+
</template>
126+
127+
<script>
128+
import { ref } from 'vue';
129+
130+
const buttonRef = ref();
131+
const link = useTemplateRef('link');
132+
</script>
133+
`,
134+
errors: [
135+
{
136+
messageId: 'preferUseTemplateRef',
137+
line: 11,
138+
column: 22
139+
}
140+
]
141+
},
142+
{
143+
filename: 'multiple-refs.vue',
144+
code: `
145+
<template>
146+
<h1 ref="heading">Heading</h1>
147+
<a href="" ref="link">Link</a>
148+
</template>
149+
150+
<script>
151+
import { ref } from 'vue';
152+
153+
const heading = ref();
154+
const link = ref();
155+
</script>
156+
`,
157+
output: `
158+
<template>
159+
<h1 ref="heading">Heading</h1>
160+
<a href="" ref="link">Link</a>
161+
</template>
162+
163+
<script>
164+
import { ref } from 'vue';
165+
166+
const heading = useTemplateRef('heading');
167+
const link = useTemplateRef('link');
168+
</script>
169+
`,
170+
errors: [
171+
{
172+
messageId: 'preferUseTemplateRef',
173+
line: 10,
174+
column: 25
175+
},
176+
{
177+
messageId: 'preferUseTemplateRef',
178+
line: 11,
179+
column: 22
180+
}
181+
]
182+
},
183+
{
184+
filename: 'ref-in-block.vue',
185+
code: `
186+
<template>
187+
<div>
188+
<ul>
189+
<li ref="firstListItem">Morning</li>
190+
<li>Afternoon</li>
191+
<li>Evening</li>
192+
</ul>
193+
</div>
194+
</template>
195+
196+
<script>
197+
import { ref } from 'vue';
198+
199+
function getFirstListItemElement() {
200+
const firstListItem = ref();
201+
}
202+
</script>
203+
`,
204+
output: `
205+
<template>
206+
<div>
207+
<ul>
208+
<li ref="firstListItem">Morning</li>
209+
<li>Afternoon</li>
210+
<li>Evening</li>
211+
</ul>
212+
</div>
213+
</template>
214+
215+
<script>
216+
import { ref } from 'vue';
217+
218+
function getFirstListItemElement() {
219+
const firstListItem = useTemplateRef('firstListItem');
220+
}
221+
</script>
222+
`,
223+
errors: [
224+
{
225+
messageId: 'preferUseTemplateRef',
226+
line: 16,
227+
column: 33
228+
}
229+
]
230+
}
231+
]
232+
})

0 commit comments

Comments
 (0)