feat: support casting plain element to component via is="vue:xxx"

In Vue 3's custom elements interop, we no longer process `is` usage on
known native elements as component casting. (ref:
https://v3.vuejs.org/guide/migration/custom-elements-interop.html)
This introduced the need for `v-is`. However, since it is a directive,
its value is considered a JavaScript expression. This makes it awkward
to use (e.g. `v-is="'foo'"`) when majority of casting is non-dynamic,
and also hinders static analysis when casting to built-in Vue
components, e.g. transition-group.

This commit adds the ability to cast a native element to a Vue component
by simply adding a `vue:` prefix:

```html
<button is="vue:my-button"></button>
<ul is="vue:transition-group" tag="ul"></ul>
```
This commit is contained in:
Evan You 2021-04-12 13:07:59 -04:00
parent 422b13e798
commit af9e6999e1
2 changed files with 30 additions and 15 deletions

View File

@ -488,7 +488,12 @@ function parseTag(
const options = context.options const options = context.options
if (!context.inVPre && !options.isCustomElement(tag)) { if (!context.inVPre && !options.isCustomElement(tag)) {
const hasVIs = props.some( const hasVIs = props.some(
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is' p =>
p.name === 'is' &&
// v-is="xxx" (TODO: deprecate)
(p.type === NodeTypes.DIRECTIVE ||
// is="vue:xxx"
(p.value && p.value.content.startsWith('vue:')))
) )
if (options.isNativeTag && !hasVIs) { if (options.isNativeTag && !hasVIs) {
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT

View File

@ -230,13 +230,19 @@ export function resolveComponentType(
context: TransformContext, context: TransformContext,
ssr = false ssr = false
) { ) {
const { tag } = node let { tag } = node
// 1. dynamic component // 1. dynamic component
const isProp = isComponentTag(tag) const isExplicitDynamic = isComponentTag(tag)
? findProp(node, 'is') const isProp =
: findDir(node, 'is') findProp(node, 'is') || (!isExplicitDynamic && findDir(node, 'is'))
if (isProp) { if (isProp) {
if (!isExplicitDynamic && isProp.type === NodeTypes.ATTRIBUTE) {
// <button is="vue:xxx">
// if not <component>, only is value that starts with "vue:" will be
// treated as component by the parse phase and reach here.
tag = isProp.value!.content.slice(4)
} else {
const exp = const exp =
isProp.type === NodeTypes.ATTRIBUTE isProp.type === NodeTypes.ATTRIBUTE
? isProp.value && createSimpleExpression(isProp.value.content, true) ? isProp.value && createSimpleExpression(isProp.value.content, true)
@ -247,6 +253,7 @@ export function resolveComponentType(
]) ])
} }
} }
}
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...) // 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag) const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
@ -416,8 +423,11 @@ export function buildProps(
isStatic = false isStatic = false
} }
} }
// skip :is on <component> // skip is on <component>, or is="vue:xxx"
if (name === 'is' && isComponentTag(tag)) { if (
name === 'is' &&
(isComponentTag(tag) || (value && value.content.startsWith('vue:')))
) {
continue continue
} }
properties.push( properties.push(