diff --git a/docs/rules/index.md b/docs/rules/index.md index 1ba5978d2..4085ce237 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -270,6 +270,7 @@ For example: | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: | | [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: | | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: | +| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref` for template refs | | :hammer: | | [vue/require-default-export](./require-default-export.md) | require components to be the default export | | :warning: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | :hammer: | | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: | diff --git a/docs/rules/prefer-use-template-ref.md b/docs/rules/prefer-use-template-ref.md new file mode 100644 index 000000000..6d7fde89a --- /dev/null +++ b/docs/rules/prefer-use-template-ref.md @@ -0,0 +1,71 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/prefer-use-template-ref +description: require using `useTemplateRef` instead of `ref` for template refs +--- + +# vue/prefer-use-template-ref + +> require using `useTemplateRef` instead of `ref` for template refs + +- :exclamation: _**This rule has not been released yet.**_ + +## :book: Rule Details + +Vue 3.5 introduced a new way of obtaining template refs via +the [`useTemplateRef()`](https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs) API. + +This rule enforces using the new `useTemplateRef` function instead of `ref` for template refs. + + + +```vue + + Submit + Close + + + +``` + + + +This rule skips `ref` template function refs as these should be used to allow custom implementation of storing `ref`. If you prefer +`useTemplateRef`, you have to change the value of the template `ref` to a string. + + + +```vue + + Content + + + +``` + + + +## :wrench: Options + +Nothing. + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-use-template-ref.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-use-template-ref.js) diff --git a/lib/index.js b/lib/index.js index bb4abf40f..b09e247d5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -208,6 +208,7 @@ const plugin = { 'prefer-separate-static-class': require('./rules/prefer-separate-static-class'), 'prefer-template': require('./rules/prefer-template'), 'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'), + 'prefer-use-template-ref': require('./rules/prefer-use-template-ref'), 'prop-name-casing': require('./rules/prop-name-casing'), 'quote-props': require('./rules/quote-props'), 'require-component-is': require('./rules/require-component-is'), diff --git a/lib/rules/prefer-use-template-ref.js b/lib/rules/prefer-use-template-ref.js new file mode 100644 index 000000000..8dcdccb38 --- /dev/null +++ b/lib/rules/prefer-use-template-ref.js @@ -0,0 +1,89 @@ +/** + * @author Thomasan1999 + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +/** @param expression {Expression | null} */ +function expressionIsRef(expression) { + // @ts-ignore + return expression?.callee?.name === 'ref' +} + +/** @type {import("eslint").Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'require using `useTemplateRef` instead of `ref` for template refs', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html' + }, + schema: [], + messages: { + preferUseTemplateRef: "Replace 'ref' with 'useTemplateRef'." + } + }, + /** @param {RuleContext} context */ + create(context) { + /** @type Set */ + const templateRefs = new Set() + + /** + * @typedef ScriptRef + * @type {{node: Expression, ref: string}} + */ + + /** + * @type ScriptRef[] */ + const scriptRefs = [] + + return utils.compositingVisitors( + utils.defineTemplateBodyVisitor( + context, + { + 'VAttribute[directive=false]'(node) { + if (node.key.name === 'ref' && node.value?.value) { + templateRefs.add(node.value.value) + } + } + }, + { + VariableDeclarator(declarator) { + if (!expressionIsRef(declarator.init)) { + return + } + + scriptRefs.push({ + // @ts-ignore + node: declarator.init, + // @ts-ignore + ref: declarator.id.name + }) + } + } + ), + { + 'Program:exit'() { + for (const templateRef of templateRefs) { + const scriptRef = scriptRefs.find( + (scriptRef) => scriptRef.ref === templateRef + ) + + if (!scriptRef) { + continue + } + + context.report({ + node: scriptRef.node, + messageId: 'preferUseTemplateRef' + }) + } + } + } + ) + } +} diff --git a/tests/lib/rules/prefer-use-template-ref.js b/tests/lib/rules/prefer-use-template-ref.js new file mode 100644 index 000000000..49a2f0759 --- /dev/null +++ b/tests/lib/rules/prefer-use-template-ref.js @@ -0,0 +1,323 @@ +/** + * @author Thomasan1999 + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/prefer-use-template-ref') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('prefer-use-template-ref', rule, { + valid: [ + { + filename: 'single-use-template-ref.vue', + code: ` + + + + + ` + }, + { + filename: 'multiple-use-template-refs.vue', + code: ` + + Content + Link + + + ` + }, + { + filename: 'use-template-ref-in-block.vue', + code: ` + + + + Morning + Afternoon + Evening + + + + + ` + }, + { + filename: 'non-template-ref.vue', + code: ` + + + + {{food}} + + + + + ` + }, + { + filename: 'counter.js', + code: ` + import { ref } from 'vue'; + const counter = ref(0); + const names = ref(new Set()); + function incrementCounter() { + counter.value++; + return counter.value; + } + function storeName(name) { + names.value.add(name) + } + ` + }, + { + filename: 'setup-function.vue', + code: ` + + Button clicked {{counter}} times. + Click + + + ` + }, + { + filename: 'options-api-no-refs.vue', + code: ` + + + Name: + + + {{niceName}} + + + ` + }, + { + filename: 'options-api-mixed.vue', + code: ` + + + Name: + + + {{ loremIpsum }} + + + ` + }, + { + filename: 'template-ref-function.vue', + code: ` + + Content + + + ` + } + ], + invalid: [ + { + filename: 'single-ref.vue', + code: ` + + + + + `, + errors: [ + { + messageId: 'preferUseTemplateRef', + line: 7, + column: 22 + } + ] + }, + { + filename: 'one-ref-unused-in-script.vue', + code: ` + + Content + Link + + + `, + errors: [ + { + messageId: 'preferUseTemplateRef', + line: 9, + column: 22 + } + ] + }, + { + filename: 'multiple-refs.vue', + code: ` + + Heading + Link + + + `, + errors: [ + { + messageId: 'preferUseTemplateRef', + line: 8, + column: 25 + }, + { + messageId: 'preferUseTemplateRef', + line: 9, + column: 22 + } + ] + }, + { + filename: 'ref-in-block.vue', + code: ` + + + + Morning + Afternoon + Evening + + + + + `, + errors: [ + { + messageId: 'preferUseTemplateRef', + line: 14, + column: 33 + } + ] + }, + { + filename: 'setup-function-only-refs.vue', + code: ` + + Button clicked {{counter}} times. + Click + + + `, + errors: [ + { + messageId: 'preferUseTemplateRef', + line: 12, + column: 28 + } + ] + } + ] +})
Button clicked {{counter}} times.
{{niceName}}