feat(core): support dynamic component via <component :is> (#320)
This commit is contained in:
parent
d179918001
commit
7f23eaf661
@ -7,7 +7,8 @@ import {
|
|||||||
APPLY_DIRECTIVES,
|
APPLY_DIRECTIVES,
|
||||||
TO_HANDLERS,
|
TO_HANDLERS,
|
||||||
helperNameMap,
|
helperNameMap,
|
||||||
PORTAL
|
PORTAL,
|
||||||
|
RESOLVE_DYNAMIC_COMPONENT
|
||||||
} from '../../src/runtimeHelpers'
|
} from '../../src/runtimeHelpers'
|
||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
@ -47,6 +48,14 @@ function parseWithElementTransform(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseWithBind(template: string) {
|
||||||
|
return parseWithElementTransform(template, {
|
||||||
|
directiveTransforms: {
|
||||||
|
bind: transformBind
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
describe('compiler: element transform', () => {
|
describe('compiler: element transform', () => {
|
||||||
test('import + resolve component', () => {
|
test('import + resolve component', () => {
|
||||||
const { root } = parseWithElementTransform(`<Foo/>`)
|
const { root } = parseWithElementTransform(`<Foo/>`)
|
||||||
@ -626,14 +635,6 @@ describe('compiler: element transform', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('patchFlag analysis', () => {
|
describe('patchFlag analysis', () => {
|
||||||
function parseWithBind(template: string) {
|
|
||||||
return parseWithElementTransform(template, {
|
|
||||||
directiveTransforms: {
|
|
||||||
bind: transformBind
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
test('TEXT', () => {
|
test('TEXT', () => {
|
||||||
const { node } = parseWithBind(`<div>foo</div>`)
|
const { node } = parseWithBind(`<div>foo</div>`)
|
||||||
expect(node.arguments.length).toBe(3)
|
expect(node.arguments.length).toBe(3)
|
||||||
@ -717,4 +718,31 @@ describe('compiler: element transform', () => {
|
|||||||
expect(vnodeCall.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
|
expect(vnodeCall.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('dynamic component', () => {
|
||||||
|
test('static binding', () => {
|
||||||
|
const { node, root } = parseWithBind(`<component is="foo" />`)
|
||||||
|
expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT)
|
||||||
|
expect(node).toMatchObject({
|
||||||
|
callee: CREATE_VNODE,
|
||||||
|
arguments: ['_component_foo']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('dynamic binding', () => {
|
||||||
|
const { node, root } = parseWithBind(`<component :is="foo" />`)
|
||||||
|
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
|
||||||
|
expect(node.arguments).toMatchObject([
|
||||||
|
{
|
||||||
|
callee: RESOLVE_DYNAMIC_COMPONENT,
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
type: NodeTypes.SIMPLE_EXPRESSION,
|
||||||
|
content: 'foo'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -7,6 +7,9 @@ export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``)
|
|||||||
export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
|
export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
|
||||||
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
|
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
|
||||||
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
|
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
|
||||||
|
export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
|
||||||
|
__DEV__ ? `resolveDynamicComponent` : ``
|
||||||
|
)
|
||||||
export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``)
|
export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``)
|
||||||
export const APPLY_DIRECTIVES = Symbol(__DEV__ ? `applyDirectives` : ``)
|
export const APPLY_DIRECTIVES = Symbol(__DEV__ ? `applyDirectives` : ``)
|
||||||
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
|
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
|
||||||
@ -30,6 +33,7 @@ export const helperNameMap: any = {
|
|||||||
[CREATE_BLOCK]: `createBlock`,
|
[CREATE_BLOCK]: `createBlock`,
|
||||||
[CREATE_VNODE]: `createVNode`,
|
[CREATE_VNODE]: `createVNode`,
|
||||||
[RESOLVE_COMPONENT]: `resolveComponent`,
|
[RESOLVE_COMPONENT]: `resolveComponent`,
|
||||||
|
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
|
||||||
[RESOLVE_DIRECTIVE]: `resolveDirective`,
|
[RESOLVE_DIRECTIVE]: `resolveDirective`,
|
||||||
[APPLY_DIRECTIVES]: `applyDirectives`,
|
[APPLY_DIRECTIVES]: `applyDirectives`,
|
||||||
[RENDER_LIST]: `renderList`,
|
[RENDER_LIST]: `renderList`,
|
||||||
|
@ -22,12 +22,13 @@ import {
|
|||||||
APPLY_DIRECTIVES,
|
APPLY_DIRECTIVES,
|
||||||
RESOLVE_DIRECTIVE,
|
RESOLVE_DIRECTIVE,
|
||||||
RESOLVE_COMPONENT,
|
RESOLVE_COMPONENT,
|
||||||
|
RESOLVE_DYNAMIC_COMPONENT,
|
||||||
MERGE_PROPS,
|
MERGE_PROPS,
|
||||||
TO_HANDLERS,
|
TO_HANDLERS,
|
||||||
PORTAL,
|
PORTAL,
|
||||||
SUSPENSE
|
SUSPENSE
|
||||||
} from '../runtimeHelpers'
|
} from '../runtimeHelpers'
|
||||||
import { getInnerRange, isVSlot, toValidAssetId } from '../utils'
|
import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils'
|
||||||
import { buildSlots } from './vSlot'
|
import { buildSlots } from './vSlot'
|
||||||
import { isStaticNode } from './hoistStatic'
|
import { isStaticNode } from './hoistStatic'
|
||||||
|
|
||||||
@ -55,24 +56,55 @@ export const transformElement: NodeTransform = (node, context) => {
|
|||||||
let patchFlag: number = 0
|
let patchFlag: number = 0
|
||||||
let runtimeDirectives: DirectiveNode[] | undefined
|
let runtimeDirectives: DirectiveNode[] | undefined
|
||||||
let dynamicPropNames: string[] | undefined
|
let dynamicPropNames: string[] | undefined
|
||||||
|
let dynamicComponent: string | CallExpression | undefined
|
||||||
|
|
||||||
if (isComponent) {
|
// handle dynamic component
|
||||||
|
const isProp = findProp(node, 'is')
|
||||||
|
if (node.tag === 'component') {
|
||||||
|
if (isProp) {
|
||||||
|
// static <component is="foo" />
|
||||||
|
if (isProp.type === NodeTypes.ATTRIBUTE) {
|
||||||
|
const tag = isProp.value && isProp.value.content
|
||||||
|
if (tag) {
|
||||||
|
context.helper(RESOLVE_COMPONENT)
|
||||||
|
context.components.add(tag)
|
||||||
|
dynamicComponent = toValidAssetId(tag, `component`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// dynamic <component :is="asdf" />
|
||||||
|
else if (isProp.exp) {
|
||||||
|
dynamicComponent = createCallExpression(
|
||||||
|
context.helper(RESOLVE_DYNAMIC_COMPONENT),
|
||||||
|
[isProp.exp]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isComponent && !dynamicComponent) {
|
||||||
context.helper(RESOLVE_COMPONENT)
|
context.helper(RESOLVE_COMPONENT)
|
||||||
context.components.add(node.tag)
|
context.components.add(node.tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
const args: CallExpression['arguments'] = [
|
const args: CallExpression['arguments'] = [
|
||||||
isComponent
|
dynamicComponent
|
||||||
? toValidAssetId(node.tag, `component`)
|
? dynamicComponent
|
||||||
: node.tagType === ElementTypes.PORTAL
|
: isComponent
|
||||||
? context.helper(PORTAL)
|
? toValidAssetId(node.tag, `component`)
|
||||||
: node.tagType === ElementTypes.SUSPENSE
|
: node.tagType === ElementTypes.PORTAL
|
||||||
? context.helper(SUSPENSE)
|
? context.helper(PORTAL)
|
||||||
: `"${node.tag}"`
|
: node.tagType === ElementTypes.SUSPENSE
|
||||||
|
? context.helper(SUSPENSE)
|
||||||
|
: `"${node.tag}"`
|
||||||
]
|
]
|
||||||
// props
|
// props
|
||||||
if (hasProps) {
|
if (hasProps) {
|
||||||
const propsBuildResult = buildProps(node, context)
|
const propsBuildResult = buildProps(
|
||||||
|
node,
|
||||||
|
context,
|
||||||
|
// skip reserved "is" prop <component is>
|
||||||
|
node.props.filter(p => p !== isProp)
|
||||||
|
)
|
||||||
patchFlag = propsBuildResult.patchFlag
|
patchFlag = propsBuildResult.patchFlag
|
||||||
dynamicPropNames = propsBuildResult.dynamicPropNames
|
dynamicPropNames = propsBuildResult.dynamicPropNames
|
||||||
runtimeDirectives = propsBuildResult.directives
|
runtimeDirectives = propsBuildResult.directives
|
||||||
|
@ -5,7 +5,8 @@ import {
|
|||||||
resolveComponent,
|
resolveComponent,
|
||||||
resolveDirective,
|
resolveDirective,
|
||||||
Component,
|
Component,
|
||||||
Directive
|
Directive,
|
||||||
|
resolveDynamicComponent
|
||||||
} from '@vue/runtime-test'
|
} from '@vue/runtime-test'
|
||||||
|
|
||||||
describe('resolveAssets', () => {
|
describe('resolveAssets', () => {
|
||||||
@ -90,5 +91,30 @@ describe('resolveAssets', () => {
|
|||||||
expect('Failed to resolve component: foo').toHaveBeenWarned()
|
expect('Failed to resolve component: foo').toHaveBeenWarned()
|
||||||
expect('Failed to resolve directive: bar').toHaveBeenWarned()
|
expect('Failed to resolve directive: bar').toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('resolve dynamic component', () => {
|
||||||
|
const app = createApp()
|
||||||
|
const dynamicComponents = {
|
||||||
|
foo: () => 'foo',
|
||||||
|
bar: () => 'bar',
|
||||||
|
baz: { render: () => 'baz' }
|
||||||
|
}
|
||||||
|
let foo, bar, baz // dynamic components
|
||||||
|
const Root = {
|
||||||
|
components: { foo: dynamicComponents.foo },
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
foo = resolveDynamicComponent('foo') // <component is="foo"/>
|
||||||
|
bar = resolveDynamicComponent(dynamicComponents.bar) // <component :is="bar"/>, function
|
||||||
|
baz = resolveDynamicComponent(dynamicComponents.baz) // <component :is="baz"/>, object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
app.mount(Root, root)
|
||||||
|
expect(foo).toBe(dynamicComponents.foo)
|
||||||
|
expect(bar).toBe(dynamicComponents.bar)
|
||||||
|
expect(baz).toBe(dynamicComponents.baz)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,13 +1,30 @@
|
|||||||
import { currentRenderingInstance } from '../componentRenderUtils'
|
import { currentRenderingInstance } from '../componentRenderUtils'
|
||||||
import { currentInstance, Component } from '../component'
|
import { currentInstance, Component } from '../component'
|
||||||
import { Directive } from '../directives'
|
import { Directive } from '../directives'
|
||||||
import { camelize, capitalize } from '@vue/shared'
|
import {
|
||||||
|
camelize,
|
||||||
|
capitalize,
|
||||||
|
isString,
|
||||||
|
isObject,
|
||||||
|
isFunction
|
||||||
|
} from '@vue/shared'
|
||||||
import { warn } from '../warning'
|
import { warn } from '../warning'
|
||||||
|
|
||||||
export function resolveComponent(name: string): Component | undefined {
|
export function resolveComponent(name: string): Component | undefined {
|
||||||
return resolveAsset('components', name)
|
return resolveAsset('components', name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveDynamicComponent(
|
||||||
|
component: unknown
|
||||||
|
): Component | undefined {
|
||||||
|
if (!component) return
|
||||||
|
if (isString(component)) {
|
||||||
|
return resolveAsset('components', component)
|
||||||
|
} else if (isFunction(component) || isObject(component)) {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveDirective(name: string): Directive | undefined {
|
export function resolveDirective(name: string): Directive | undefined {
|
||||||
return resolveAsset('directives', name)
|
return resolveAsset('directives', name)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,11 @@ export {
|
|||||||
// Internal, for compiler generated code
|
// Internal, for compiler generated code
|
||||||
// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
|
// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
|
||||||
export { applyDirectives } from './directives'
|
export { applyDirectives } from './directives'
|
||||||
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
|
export {
|
||||||
|
resolveComponent,
|
||||||
|
resolveDirective,
|
||||||
|
resolveDynamicComponent
|
||||||
|
} from './helpers/resolveAssets'
|
||||||
export { renderList } from './helpers/renderList'
|
export { renderList } from './helpers/renderList'
|
||||||
export { toString } from './helpers/toString'
|
export { toString } from './helpers/toString'
|
||||||
export { toHandlers } from './helpers/toHandlers'
|
export { toHandlers } from './helpers/toHandlers'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user