mirror of https://github.com/vuejs/core.git
feat(vapor): dynamic component
This commit is contained in:
parent
fab9917ae4
commit
5f92ff8ca2
|
@ -213,6 +213,54 @@ export function render(_ctx) {
|
|||
}"
|
||||
`;
|
||||
|
||||
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) {
|
||||
|
|
|
@ -423,6 +423,117 @@ describe('compiler: element transform', () => {
|
|||
})
|
||||
})
|
||||
|
||||
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" />`,
|
||||
|
|
|
@ -65,7 +65,12 @@ export function genCreateComponent(
|
|||
]
|
||||
|
||||
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(
|
||||
|
|
|
@ -194,6 +194,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
|
|||
asset: boolean
|
||||
root: boolean
|
||||
once: boolean
|
||||
dynamic?: SimpleExpressionNode
|
||||
}
|
||||
|
||||
export interface DeclareOldRefIRNode extends BaseIRNode {
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
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,
|
||||
|
@ -33,6 +36,7 @@ import {
|
|||
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
|
||||
|
@ -51,46 +55,56 @@ export const transformElement: NodeTransform = (node, context) => {
|
|||
)
|
||||
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,
|
||||
) {
|
||||
const dynamicComponent = isDynamicComponent
|
||||
? resolveDynamicComponent(node)
|
||||
: undefined
|
||||
|
||||
let { tag } = node
|
||||
let asset = true
|
||||
|
||||
const fromSetup = resolveSetupReference(tag, context)
|
||||
if (fromSetup) {
|
||||
tag = fromSetup
|
||||
asset = false
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -106,10 +120,28 @@ function transformComponentElement(
|
|||
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) {
|
||||
|
@ -128,10 +160,11 @@ function resolveSetupReference(name: string, context: TransformContext) {
|
|||
}
|
||||
|
||||
function transformNativeElement(
|
||||
tag: string,
|
||||
node: PlainElementNode,
|
||||
propsResult: PropsResult,
|
||||
context: TransformContext<ElementNode>,
|
||||
) {
|
||||
const { tag } = node
|
||||
const { scopeId } = context.options
|
||||
|
||||
let template = ''
|
||||
|
@ -189,6 +222,7 @@ export function buildProps(
|
|||
node: ElementNode,
|
||||
context: TransformContext<ElementNode>,
|
||||
isComponent: boolean,
|
||||
isDynamicComponent: boolean,
|
||||
): PropsResult {
|
||||
const props = node.props as (VaporDirectiveNode | AttributeNode)[]
|
||||
if (props.length === 0) return [false, []]
|
||||
|
@ -252,6 +286,18 @@ export function buildProps(
|
|||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
@ -362,3 +408,7 @@ function mergePropValues(existing: IRProp, incoming: IRProp) {
|
|||
const newValues = incoming.values
|
||||
existing.values.push(...newValues)
|
||||
}
|
||||
|
||||
function isComponentTag(tag: string) {
|
||||
return tag === 'component' || tag === 'Component'
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
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'
|
||||
|
@ -79,3 +79,16 @@ function resolve(registry: Record<string, any> | undefined, name: string) {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,7 +131,11 @@ export { createFor, createForSlots } from './apiCreateFor'
|
|||
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'
|
||||
|
|
Loading…
Reference in New Issue