Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: v-slot-style rule (fixes #801) #836

Merged
merged 2 commits into from Mar 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
110 changes: 110 additions & 0 deletions docs/rules/v-slot-style.md
@@ -0,0 +1,110 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/v-slot-style
description: enforce `v-slot` directive style
---
# vue/v-slot-style
> enforce `v-slot` directive style

- :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.

## :book: Rule Details

This rule enforces `v-slot` directive style which you should use shorthand or long form.

<eslint-code-block fix :rules="{'vue/v-slot-style': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<my-component v-slot="data">
{{data}}
</my-component>
<my-component>
<template #default>content</template>
<template #one>content</template>
<template #two>content</template>
</my-component>

<!-- ✗ BAD -->
<my-component #default="data">
{{data}}
</my-component>
<my-component>
<template v-slot>content</template>
<template v-slot:one>content</template>
<template v-slot:two>content</template>
</my-component>
</template>
```

</eslint-code-block>

## :wrench: Options

```json
{
"vue/v-slot-style": ["error", {
"atComponent": "shorthand" | "longform" | "v-slot",
"default": "shorthand" | "longform" | "v-slot",
"named": "shorthand" | "longform",
}]
}
```

| Name | Type | Default Value | Description
|:-----|:-----|:--------------|:------------
| `atComponent` | `"shorthand"` \| `"longform"` \| `"v-slot"` | `"v-slot"` | The style for the default slot at custom components directly (E.g. `<my-component v-slot="">`).
| `default` | `"shorthand"` \| `"longform"` \| `"v-slot"` | `"shorthand"` | The style for the default slot at template wrappers (E.g. `<template #default="">`).
| `named` | `"shorthand"` \| `"longform"` | `"shorthand"` | The style for named slots (E.g. `<template #named="">`).

Each value means:

- `"shorthand"` ... use `#` shorthand. E.g. `#default`, `#named`, ...
- `"longform"` ... use `v-slot:` directive notation. E.g. `v-slot:default`, `v-slot:named`, ...
- `"v-slot"` ... use `v-slot` without that argument. This is shorter than `#default` shorthand.

And a string option is supported to be consistent to similar `vue/v-bind-style` and `vue/v-on-style`.

- `["error", "longform"]` is same as `["error", { atComponent: "longform", default: "longform", named: "longform" }]`.
- `["error", "shorthand"]` is same as `["error", { atComponent: "shorthand", default: "shorthand", named: "shorthand" }]`.

### `"longform"`

<eslint-code-block fix :rules="{'vue/v-slot-style': ['error', 'longform']}">

```vue
<template>
<!-- ✓ GOOD -->
<my-component v-slot:default="data">
{{data}}
</my-component>
<my-component>
<template v-slot:default>content</template>
<template v-slot:one>content</template>
<template v-slot:two>content</template>
</my-component>

<!-- ✗ BAD -->
<my-component v-slot="data">
{{data}}
</my-component>
<my-component>
<template #default>content</template>
<template #one>content</template>
<template #two>content</template>
</my-component>
</template>
```

</eslint-code-block>

## :books: Further reading

- [Style guide - Directive shorthands](https://vuejs.org/v2/style-guide/#Directive-shorthands-strongly-recommended)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-slot-style.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/v-slot-style.js)
146 changes: 146 additions & 0 deletions lib/rules/v-slot-style.js
@@ -0,0 +1,146 @@
/**
* @author Toru Nagashima
* See LICENSE file in root directory for full license.
*/
'use strict'

const { pascalCase } = require('../utils/casing')
const utils = require('../utils')

/**
* @typedef {Object} Options
* @property {"shorthand" | "longform" | "v-slot"} atComponent The style for the default slot at a custom component directly.
* @property {"shorthand" | "longform" | "v-slot"} default The style for the default slot at a template wrapper.
* @property {"shorthand" | "longform"} named The style for named slots at a template wrapper.
*/

/**
* Normalize options.
* @param {any} options The raw options to normalize.
* @returns {Options} The normalized options.
*/
function normalizeOptions (options) {
const normalized = {
atComponent: 'v-slot',
default: 'shorthand',
named: 'shorthand'
}

if (typeof options === 'string') {
normalized.atComponent = normalized.default = normalized.named = options
} else if (options != null) {
for (const key of ['atComponent', 'default', 'named']) {
if (options[key] != null) {
normalized[key] = options[key]
}
}
}

return normalized
}

/**
* Get the expected style.
* @param {Options} options The options that defined expected types.
* @param {VAttribute} node The `v-slot` node to check.
* @returns {"shorthand" | "longform" | "v-slot"} The expected style.
*/
function getExpectedStyle (options, node) {
const { argument } = node.key

if (argument == null || (argument.type === 'VIdentifier' && argument.name === 'default')) {
const element = node.parent.parent
return element.name === 'template' ? options.default : options.atComponent
}
return options.named
}

/**
* Get the expected style.
* @param {VAttribute} node The `v-slot` node to check.
* @returns {"shorthand" | "longform" | "v-slot"} The expected style.
*/
function getActualStyle (node) {
const { name, argument } = node.key

if (name.rawName === '#') {
return 'shorthand'
}
if (argument != null) {
return 'longform'
}
return 'v-slot'
}

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce `v-slot` directive style',
category: undefined, // strongly-recommended
url: 'https://eslint.vuejs.org/rules/v-slot-style.html'
},
fixable: 'code',
schema: [
{
anyOf: [
{ enum: ['shorthand', 'longform'] },
{
type: 'object',
properties: {
atComponent: { enum: ['shorthand', 'longform', 'v-slot'] },
default: { enum: ['shorthand', 'longform', 'v-slot'] },
named: { enum: ['shorthand', 'longform'] }
},
additionalProperties: false
}
]
}
],
messages: {
expectedShorthand: "Expected '#{{argument}}' instead of '{{actual}}'.",
expectedLongform: "Expected 'v-slot:{{argument}}' instead of '{{actual}}'.",
expectedVSlot: "Expected 'v-slot' instead of '{{actual}}'."
}
},

create (context) {
const sourceCode = context.getSourceCode()
const options = normalizeOptions(context.options[0])

return utils.defineTemplateBodyVisitor(context, {
"VAttribute[directive=true][key.name.name='slot']" (node) {
const expected = getExpectedStyle(options, node)
const actual = getActualStyle(node)
if (actual === expected) {
return
}

const { name, argument } = node.key
const range = [name.range[0], (argument || name).range[1]]
const argumentText = argument ? sourceCode.getText(argument) : 'default'
context.report({
node,
messageId: `expected${pascalCase(expected)}`,
data: {
actual: sourceCode.text.slice(range[0], range[1]),
argument: argumentText
},

fix (fixer) {
switch (expected) {
case 'shorthand':
return fixer.replaceTextRange(range, `#${argumentText}`)
case 'longform':
return fixer.replaceTextRange(range, `v-slot:${argumentText}`)
case 'v-slot':
return fixer.replaceTextRange(range, 'v-slot')
default:
return null
}
}
})
}
})
}
}