feat(vapor): dynamic component

This commit is contained in:
三咲智子 Kevin Deng 2024-11-13 14:56:39 +08:00
parent fab9917ae4
commit 5f92ff8ca2
No known key found for this signature in database
7 changed files with 254 additions and 22 deletions

View File

@ -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) {

View File

@ -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" />`,

View File

@ -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(

View File

@ -194,6 +194,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
asset: boolean
root: boolean
once: boolean
dynamic?: SimpleExpressionNode
}
export interface DeclareOldRefIRNode extends BaseIRNode {

View File

@ -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'
}

View File

@ -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
}
}

View File

@ -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'