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,
|
||||
TO_HANDLERS,
|
||||
helperNameMap,
|
||||
PORTAL
|
||||
PORTAL,
|
||||
RESOLVE_DYNAMIC_COMPONENT
|
||||
} from '../../src/runtimeHelpers'
|
||||
import {
|
||||
CallExpression,
|
||||
@ -47,6 +48,14 @@ function parseWithElementTransform(
|
||||
}
|
||||
}
|
||||
|
||||
function parseWithBind(template: string) {
|
||||
return parseWithElementTransform(template, {
|
||||
directiveTransforms: {
|
||||
bind: transformBind
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('compiler: element transform', () => {
|
||||
test('import + resolve component', () => {
|
||||
const { root } = parseWithElementTransform(`<Foo/>`)
|
||||
@ -626,14 +635,6 @@ describe('compiler: element transform', () => {
|
||||
})
|
||||
|
||||
describe('patchFlag analysis', () => {
|
||||
function parseWithBind(template: string) {
|
||||
return parseWithElementTransform(template, {
|
||||
directiveTransforms: {
|
||||
bind: transformBind
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('TEXT', () => {
|
||||
const { node } = parseWithBind(`<div>foo</div>`)
|
||||
expect(node.arguments.length).toBe(3)
|
||||
@ -717,4 +718,31 @@ describe('compiler: element transform', () => {
|
||||
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_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
|
||||
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
|
||||
export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
|
||||
__DEV__ ? `resolveDynamicComponent` : ``
|
||||
)
|
||||
export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``)
|
||||
export const APPLY_DIRECTIVES = Symbol(__DEV__ ? `applyDirectives` : ``)
|
||||
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
|
||||
@ -30,6 +33,7 @@ export const helperNameMap: any = {
|
||||
[CREATE_BLOCK]: `createBlock`,
|
||||
[CREATE_VNODE]: `createVNode`,
|
||||
[RESOLVE_COMPONENT]: `resolveComponent`,
|
||||
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
|
||||
[RESOLVE_DIRECTIVE]: `resolveDirective`,
|
||||
[APPLY_DIRECTIVES]: `applyDirectives`,
|
||||
[RENDER_LIST]: `renderList`,
|
||||
|
@ -22,12 +22,13 @@ import {
|
||||
APPLY_DIRECTIVES,
|
||||
RESOLVE_DIRECTIVE,
|
||||
RESOLVE_COMPONENT,
|
||||
RESOLVE_DYNAMIC_COMPONENT,
|
||||
MERGE_PROPS,
|
||||
TO_HANDLERS,
|
||||
PORTAL,
|
||||
SUSPENSE
|
||||
} from '../runtimeHelpers'
|
||||
import { getInnerRange, isVSlot, toValidAssetId } from '../utils'
|
||||
import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils'
|
||||
import { buildSlots } from './vSlot'
|
||||
import { isStaticNode } from './hoistStatic'
|
||||
|
||||
@ -55,24 +56,55 @@ export const transformElement: NodeTransform = (node, context) => {
|
||||
let patchFlag: number = 0
|
||||
let runtimeDirectives: DirectiveNode[] | 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.components.add(node.tag)
|
||||
}
|
||||
|
||||
const args: CallExpression['arguments'] = [
|
||||
isComponent
|
||||
? toValidAssetId(node.tag, `component`)
|
||||
: node.tagType === ElementTypes.PORTAL
|
||||
? context.helper(PORTAL)
|
||||
: node.tagType === ElementTypes.SUSPENSE
|
||||
? context.helper(SUSPENSE)
|
||||
: `"${node.tag}"`
|
||||
dynamicComponent
|
||||
? dynamicComponent
|
||||
: isComponent
|
||||
? toValidAssetId(node.tag, `component`)
|
||||
: node.tagType === ElementTypes.PORTAL
|
||||
? context.helper(PORTAL)
|
||||
: node.tagType === ElementTypes.SUSPENSE
|
||||
? context.helper(SUSPENSE)
|
||||
: `"${node.tag}"`
|
||||
]
|
||||
// props
|
||||
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
|
||||
dynamicPropNames = propsBuildResult.dynamicPropNames
|
||||
runtimeDirectives = propsBuildResult.directives
|
||||
|
@ -5,7 +5,8 @@ import {
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
Component,
|
||||
Directive
|
||||
Directive,
|
||||
resolveDynamicComponent
|
||||
} from '@vue/runtime-test'
|
||||
|
||||
describe('resolveAssets', () => {
|
||||
@ -90,5 +91,30 @@ describe('resolveAssets', () => {
|
||||
expect('Failed to resolve component: foo').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 { currentInstance, Component } from '../component'
|
||||
import { Directive } from '../directives'
|
||||
import { camelize, capitalize } from '@vue/shared'
|
||||
import {
|
||||
camelize,
|
||||
capitalize,
|
||||
isString,
|
||||
isObject,
|
||||
isFunction
|
||||
} from '@vue/shared'
|
||||
import { warn } from '../warning'
|
||||
|
||||
export function resolveComponent(name: string): Component | undefined {
|
||||
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 {
|
||||
return resolveAsset('directives', name)
|
||||
}
|
||||
|
@ -39,7 +39,11 @@ export {
|
||||
// Internal, for compiler generated code
|
||||
// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
|
||||
export { applyDirectives } from './directives'
|
||||
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
|
||||
export {
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent
|
||||
} from './helpers/resolveAssets'
|
||||
export { renderList } from './helpers/renderList'
|
||||
export { toString } from './helpers/toString'
|
||||
export { toHandlers } from './helpers/toHandlers'
|
||||
|
Loading…
Reference in New Issue
Block a user