}"
`;
+exports[`compiler: element transform > dynamic component > capitalized version w/ static binding 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+ const n0 = _createComponent(_resolveDynamicComponent("foo"), null, null, true)
+ return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > dynamic binding 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+ const n0 = _createComponent(_resolveDynamicComponent(_ctx.foo), null, null, true)
+ return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > dynamic binding shorthand 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+ const n0 = _createComponent(_resolveDynamicComponent(_ctx.is), null, null, true)
+ return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > normal component with is prop 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+ const _component_custom_input = _resolveComponent("custom-input")
+ const n0 = _createComponent(_component_custom_input, [
+ { is: () => ("foo") }
+ ], null, true)
+ return n0
+}"
+`;
+
+exports[`compiler: element transform > dynamic component > static binding 1`] = `
+"import { resolveDynamicComponent as _resolveDynamicComponent, createComponent as _createComponent } from 'vue/vapor';
+
+export function render(_ctx) {
+ const n0 = _createComponent(_resolveDynamicComponent("foo"), null, null, true)
+ return n0
+}"
+`;
+
exports[`compiler: element transform > empty template 1`] = `
"
export function render(_ctx) {
})
})
+ describe('dynamic component', () => {
+ test('static binding', () => {
+ const { code, ir, vaporHelpers } = compileWithElementTransform(
+ `<component is="foo" />`,
+ )
+ expect(code).toMatchSnapshot()
+ expect(vaporHelpers).toContain('resolveDynamicComponent')
+ expect(ir.block.operation).toMatchObject([
+ {
+ type: IRNodeTypes.CREATE_COMPONENT_NODE,
+ tag: 'component',
+ asset: true,
+ root: true,
+ props: [[]],
+ dynamic: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'foo',
+ isStatic: true,
+ },
+ },
+ ])
+ })
+
+ test('capitalized version w/ static binding', () => {
+ const { code, ir, vaporHelpers } = compileWithElementTransform(
+ `<Component is="foo" />`,
+ )
+ expect(code).toMatchSnapshot()
+ expect(vaporHelpers).toContain('resolveDynamicComponent')
+ expect(ir.block.operation).toMatchObject([
+ {
+ type: IRNodeTypes.CREATE_COMPONENT_NODE,
+ tag: 'Component',
+ asset: true,
+ root: true,
+ props: [[]],
+ dynamic: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'foo',
+ isStatic: true,
+ },
+ },
+ ])
+ })
+
+ test('dynamic binding', () => {
+ const { code, ir, vaporHelpers } = compileWithElementTransform(
+ `<component :is="foo" />`,
+ )
+ expect(code).toMatchSnapshot()
+ expect(vaporHelpers).toContain('resolveDynamicComponent')
+ expect(ir.block.operation).toMatchObject([
+ {
+ type: IRNodeTypes.CREATE_COMPONENT_NODE,
+ tag: 'component',
+ asset: true,
+ root: true,
+ props: [[]],
+ dynamic: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'foo',
+ isStatic: false,
+ },
+ },
+ ])
+ })
+
+ test('dynamic binding shorthand', () => {
+ const { code, ir, vaporHelpers } =
+ compileWithElementTransform(`<component :is />`)
+ expect(code).toMatchSnapshot()
+ expect(vaporHelpers).toContain('resolveDynamicComponent')
+ expect(ir.block.operation).toMatchObject([
+ {
+ type: IRNodeTypes.CREATE_COMPONENT_NODE,
+ tag: 'component',
+ asset: true,
+ root: true,
+ props: [[]],
+ dynamic: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: 'is',
+ isStatic: false,
+ },
+ },
+ ])
+ })
+
+ // #3934
+ test('normal component with is prop', () => {
+ const { code, ir, vaporHelpers } = compileWithElementTransform(
+ `<custom-input is="foo" />`,
+ {
+ isNativeTag: () => false,
+ },
+ )
+ expect(code).toMatchSnapshot()
+ expect(vaporHelpers).toContain('resolveComponent')
+ expect(vaporHelpers).not.toContain('resolveDynamicComponent')
+ expect(ir.block.operation).toMatchObject([
+ {
+ type: IRNodeTypes.CREATE_COMPONENT_NODE,
+ tag: 'custom-input',
+ asset: true,
+ root: true,
+ props: [[{ key: { content: 'is' }, values: [{ content: 'foo' }] }]],
+ },
+ ])
+ })
+ })
+
test('static props', () => {
const { code, ir } = compileWithElementTransform(
`<div id="foo" class="bar" />`,
]
function genTag() {
- if (oper.asset) {
+ if (oper.dynamic) {
+ return genCall(
+ vaporHelper('resolveDynamicComponent'),
+ genExpression(oper.dynamic, context),
+ )
+ } else if (oper.asset) {
return toValidAssetId(oper.tag, 'component')
} else {
return genExpression(
asset: boolean
root: boolean
once: boolean
+ dynamic?: SimpleExpressionNode
}
export interface DeclareOldRefIRNode extends BaseIRNode {
import { isValidHTMLNesting } from '../html-nesting'
import {
type AttributeNode,
+ type ComponentNode,
type ElementNode,
ElementTypes,
ErrorCodes,
NodeTypes,
+ type PlainElementNode,
type SimpleExpressionNode,
createCompilerError,
createSimpleExpression,
+ isStaticArgOf,
} from '@vue/compiler-dom'
import {
camelize,
type VaporDirectiveNode,
} from '../ir'
import { EMPTY_EXPRESSION } from './utils'
+import { findProp } from '../utils'
export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
)
return
- const { tag, tagType } = node
- const isComponent = tagType === ElementTypes.COMPONENT
+ const isComponent = node.tagType === ElementTypes.COMPONENT
+ const isDynamicComponent = isComponentTag(node.tag)
const propsResult = buildProps(
node,
context as TransformContext<ElementNode>,
isComponent,
+ isDynamicComponent,
)
;(isComponent ? transformComponentElement : transformNativeElement)(
- tag,
+ node as any,
propsResult,
context as TransformContext<ElementNode>,
+ isDynamicComponent,
)
}
}
function transformComponentElement(
- tag: string,
+ node: ComponentNode,
propsResult: PropsResult,
context: TransformContext,
+ isDynamicComponent: boolean,
) {
- let asset = true
+ const dynamicComponent = isDynamicComponent
+ ? resolveDynamicComponent(node)
+ : undefined
- const fromSetup = resolveSetupReference(tag, context)
- if (fromSetup) {
- tag = fromSetup
- asset = false
- }
+ let { tag } = node
+ let asset = true
- const dotIndex = tag.indexOf('.')
- if (dotIndex > 0) {
- const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
- if (ns) {
- tag = ns + tag.slice(dotIndex)
+ if (!dynamicComponent) {
+ const fromSetup = resolveSetupReference(tag, context)
+ if (fromSetup) {
+ tag = fromSetup
asset = false
}
- }
- if (asset) {
- context.component.add(tag)
+ const dotIndex = tag.indexOf('.')
+ if (dotIndex > 0) {
+ const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
+ if (ns) {
+ tag = ns + tag.slice(dotIndex)
+ asset = false
+ }
+ }
+
+ if (asset) {
+ context.component.add(tag)
+ }
}
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
root,
slots: [...context.slots],
once: context.inVOnce,
+ dynamic: dynamicComponent,
})
context.slots = []
}
+function resolveDynamicComponent(node: ComponentNode) {
+ const isProp = findProp(node, 'is', false, true /* allow empty */)
+ if (!isProp) return
+
+ if (isProp.type === NodeTypes.ATTRIBUTE) {
+ return isProp.value && createSimpleExpression(isProp.value.content, true)
+ } else {
+ return (
+ isProp.exp ||
+ // #10469 handle :is shorthand
+ extend(createSimpleExpression(`is`, false, isProp.arg!.loc), {
+ ast: null,
+ })
+ )
+ }
+}
+
function resolveSetupReference(name: string, context: TransformContext) {
const bindings = context.options.bindingMetadata
if (!bindings || bindings.__isScriptSetup === false) {
}
function transformNativeElement(
- tag: string,
+ node: PlainElementNode,
propsResult: PropsResult,
context: TransformContext<ElementNode>,
) {
+ const { tag } = node
const { scopeId } = context.options
let template = ''
node: ElementNode,
context: TransformContext<ElementNode>,
isComponent: boolean,
+ isDynamicComponent: boolean,
): PropsResult {
const props = node.props as (VaporDirectiveNode | AttributeNode)[]
if (props.length === 0) return [false, []]
}
}
+ // exclude `is` prop for <component>
+ if (
+ (isDynamicComponent &&
+ prop.type === NodeTypes.ATTRIBUTE &&
+ prop.name === 'is') ||
+ (prop.type === NodeTypes.DIRECTIVE &&
+ prop.name === 'bind' &&
+ isStaticArgOf(prop.arg, 'is'))
+ ) {
+ continue
+ }
+
const result = transformProp(prop, node, context)
if (result) {
dynamicExpr.push(result.key, result.value)
const newValues = incoming.values
existing.values.push(...newValues)
}
+
+function isComponentTag(tag: string) {
+ return tag === 'component' || tag === 'Component'
+}
-import { camelize, capitalize } from '@vue/shared'
+import { camelize, capitalize, isString } from '@vue/shared'
import { type Directive, warn } from '..'
import { type Component, currentInstance } from '../component'
import { getComponentName } from '../component'
registry[capitalize(camelize(name))])
)
}
+
+/**
+ * @private
+ */
+export function resolveDynamicComponent(
+ component: string | Component,
+): string | Component {
+ if (isString(component)) {
+ return resolveAsset(COMPONENTS, component, false) || component
+ } else {
+ return component
+ }
+}
export { createComponent } from './apiCreateComponent'
export { createSelector } from './apiCreateSelector'
-export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
+export {
+ resolveComponent,
+ resolveDirective,
+ resolveDynamicComponent,
+} from './helpers/resolveAssets'
export { toHandlers } from './helpers/toHandlers'
export { withDestructure } from './destructure'