Repository: vuejs/eslint-plugin-vue (JavaScript)

Issue:
Allow custom toggles in vue/require-toggle-inside-transition 
<!--
  Before proposing rule changes, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**What rule do you want to change?**

[vue/require-toggle-inside-transition](https://eslint.vuejs.org/rules/require-toggle-inside-transition.html) 

**Does this change cause the rule to produce more or fewer warnings?**

If option is configured, fewer warnings.

**How will the change be implemented? (New option, new default behavior, etc.)?**

I would add a new option to the rule which allows adding additional custom "toggle" directives which will satisfy this check in addition to `v-show` or `v-if`.

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```vue
<transition>
  <dialog v-dialog="visible">...</dialog>
</transition>
```

**What does the rule currently do for this code?**

It flags it as an error, because there is no `v-show` or `v-if` on the `<dialog>`

**What will the rule do after it's changed?**

If the rule is configured to include `v-dialog` as a toggle directive, it will no longer flag this case as an error.

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

In the example above, `v-dialog` is a custom directive which shows/hides the element using `close()` and `showModal()`. Transitions are also applied in the directive.




Discussion:
> Transitions are also applied in the directive.

Is this documented somewhere? If not, it may be a bug or accidental behavior that might change in the future, and then I don't think we should encourage people to use that.
That's a good question. I wrote this code some time ago and can't remember exactly where I found this pattern. 😅  It may have been from mimicking `v-show`. 

The transition behavior is intentional though. The directive reads the transition property of the vnode and calls the hooks when necessary. e.g.

```
mounted(el, { value, modifiers }, { transition }) {
  if (transition && value) {
    transition.enter(el)
  }
  toggleDialog(el, value, modifiers.modal)
},
```
Okay, I guess it's fine then. A PR is welcome that:

* adds a new option `additionalDirectives: string[]` to the rule
* updates the docs
* adds new test cases
Cool, thanks for the feedback!



Patch:
diff --git a/docs/rules/require-toggle-inside-transition.md b/docs/rules/require-toggle-inside-transition.md
index c62de95cb..c0760e381 100644
--- a/docs/rules/require-toggle-inside-transition.md
+++ b/docs/rules/require-toggle-inside-transition.md
@@ -33,7 +33,34 @@ This rule reports elements inside `<transition>` that do not control the display
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+  "vue/require-toggle-inside-transition": ["error", {
+    "additionalDirectives": []
+  }]
+}
+```
+
+- `additionalDirectives` (`string[]`) ... Custom directives which will satisfy this rule in addition to `v-show` and `v-if`. Should be added without the `v-` prefix.
+
+### `additionalDirectives: ["dialog"]`
+
+<eslint-code-block :rules="{'vue/require-toggle-inside-transition': ['error', {additionalDirectives: ['dialog']}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <transition><div v-if="show" /></transition>
+  <transition><div v-show="show" /></transition>
+  <transition><dialog v-dialog="show" /></transition>
+
+  <!-- ✗ BAD -->
+  <transition><div /></transition>
+  <transition><div v-custom="show" /></transition>
+<template>
+```
+
+</eslint-code-block>
 
 ## :books: Further Reading
 
diff --git a/lib/rules/require-toggle-inside-transition.js b/lib/rules/require-toggle-inside-transition.js
index 423d4beb7..bfb267caa 100644
--- a/lib/rules/require-toggle-inside-transition.js
+++ b/lib/rules/require-toggle-inside-transition.js
@@ -20,6 +20,25 @@ function isValidBindAppear(vBindAppear) {
   return true
 }
 
+/**
+ * @param {string[]} directives
+ */
+function createDirectiveList(directives) {
+  let str = ''
+
+  for (const [index, directive] of directives.entries()) {
+    if (index === 0) {
+      str += `\`v-${directive}\``
+    } else if (index < directives.length - 1) {
+      str += `, \`v-${directive}\``
+    } else {
+      str += ` or \`v-${directive}\``
+    }
+  }
+
+  return str
+}
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -30,14 +49,34 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/require-toggle-inside-transition.html'
     },
     fixable: null,
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          additionalDirectives: {
+            type: 'array',
+            items: {
+              type: 'string'
+            },
+            uniqueItems: true
+          }
+        },
+        additionalProperties: false
+      }
+    ],
     messages: {
       expected:
-        'The element inside `<transition>` is expected to have a `v-if` or `v-show` directive.'
+        'The element inside `<transition>` is expected to have a {{allowedDirectives}} directive.'
     }
   },
   /** @param {RuleContext} context */
   create(context) {
+    /** @type {Array<string>} */
+    const additionalDirectives =
+      (context.options[0] && context.options[0].additionalDirectives) || []
+    const allowedDirectives = ['if', 'show', ...additionalDirectives]
+    const allowedDirectivesString = createDirectiveList(allowedDirectives)
+
     /**
      * Check if the given element has display control.
      * @param {VElement} element The element node to check.
@@ -59,14 +98,18 @@ module.exports = {
 
       if (
         element.name !== 'slot' &&
-        !utils.hasDirective(element, 'if') &&
-        !utils.hasDirective(element, 'show') &&
+        !allowedDirectives.some((directive) =>
+          utils.hasDirective(element, directive)
+        ) &&
         !utils.hasDirective(element, 'bind', 'key')
       ) {
         context.report({
           node: element.startTag,
           loc: element.startTag.loc,
-          messageId: 'expected'
+          messageId: 'expected',
+          data: {
+            allowedDirectives: allowedDirectivesString
+          }
         })
       }
     }


Test patch:
diff --git a/tests/lib/rules/require-toggle-inside-transition.js b/tests/lib/rules/require-toggle-inside-transition.js
index d40ad4748..cdd48c493 100644
--- a/tests/lib/rules/require-toggle-inside-transition.js
+++ b/tests/lib/rules/require-toggle-inside-transition.js
@@ -77,6 +77,15 @@ tester.run('require-toggle-inside-transition', rule, {
     {
       filename: 'test.vue',
       code: '<template><transition :appear="true"><div /></transition></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition><dialog v-dialog="show" /></transition></template>',
+      options: [
+        {
+          additionalDirectives: ['dialog']
+        }
+      ]
     }
   ],
   invalid: [
@@ -132,6 +141,36 @@ tester.run('require-toggle-inside-transition', rule, {
       filename: 'test.vue',
       code: '<template><transition @appear="isLoaded"><div /></transition></template>',
       errors: [{ messageId: 'expected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition><dialog v-dialog="show" /></transition></template>',
+      options: [
+        {
+          additionalDirectives: []
+        }
+      ],
+      errors: [
+        {
+          messageId: 'expected',
+          data: { allowedDirectives: '`v-if` or `v-show`' }
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><transition><div v-custom="show" /></transition></template>',
+      options: [
+        {
+          additionalDirectives: ['dialog']
+        }
+      ],
+      errors: [
+        {
+          messageId: 'expected',
+          data: { allowedDirectives: '`v-if`, `v-show` or `v-dialog`' }
+        }
+      ]
     }
   ]
 })