Skip to content

Commit 7f23eaf

Browse files
fnlctrlyyx990803
authored andcommitted
feat(core): support dynamic component via <component :is> (vuejs#320)
1 parent d179918 commit 7f23eaf

File tree

6 files changed

+133
-22
lines changed

6 files changed

+133
-22
lines changed

packages/compiler-core/__tests__/transforms/transformElement.spec.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
APPLY_DIRECTIVES,
88
TO_HANDLERS,
99
helperNameMap,
10-
PORTAL
10+
PORTAL,
11+
RESOLVE_DYNAMIC_COMPONENT
1112
} from '../../src/runtimeHelpers'
1213
import {
1314
CallExpression,
@@ -47,6 +48,14 @@ function parseWithElementTransform(
4748
}
4849
}
4950

51+
function parseWithBind(template: string) {
52+
return parseWithElementTransform(template, {
53+
directiveTransforms: {
54+
bind: transformBind
55+
}
56+
})
57+
}
58+
5059
describe('compiler: element transform', () => {
5160
test('import + resolve component', () => {
5261
const { root } = parseWithElementTransform(`<Foo/>`)
@@ -626,14 +635,6 @@ describe('compiler: element transform', () => {
626635
})
627636

628637
describe('patchFlag analysis', () => {
629-
function parseWithBind(template: string) {
630-
return parseWithElementTransform(template, {
631-
directiveTransforms: {
632-
bind: transformBind
633-
}
634-
})
635-
}
636-
637638
test('TEXT', () => {
638639
const { node } = parseWithBind(`<div>foo</div>`)
639640
expect(node.arguments.length).toBe(3)
@@ -717,4 +718,31 @@ describe('compiler: element transform', () => {
717718
expect(vnodeCall.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
718719
})
719720
})
721+
722+
describe('dynamic component', () => {
723+
test('static binding', () => {
724+
const { node, root } = parseWithBind(`<component is="foo" />`)
725+
expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT)
726+
expect(node).toMatchObject({
727+
callee: CREATE_VNODE,
728+
arguments: ['_component_foo']
729+
})
730+
})
731+
732+
test('dynamic binding', () => {
733+
const { node, root } = parseWithBind(`<component :is="foo" />`)
734+
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
735+
expect(node.arguments).toMatchObject([
736+
{
737+
callee: RESOLVE_DYNAMIC_COMPONENT,
738+
arguments: [
739+
{
740+
type: NodeTypes.SIMPLE_EXPRESSION,
741+
content: 'foo'
742+
}
743+
]
744+
}
745+
])
746+
})
747+
})
720748
})

packages/compiler-core/src/runtimeHelpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``)
77
export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
88
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
99
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
10+
export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
11+
__DEV__ ? `resolveDynamicComponent` : ``
12+
)
1013
export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``)
1114
export const APPLY_DIRECTIVES = Symbol(__DEV__ ? `applyDirectives` : ``)
1215
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
@@ -30,6 +33,7 @@ export const helperNameMap: any = {
3033
[CREATE_BLOCK]: `createBlock`,
3134
[CREATE_VNODE]: `createVNode`,
3235
[RESOLVE_COMPONENT]: `resolveComponent`,
36+
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
3337
[RESOLVE_DIRECTIVE]: `resolveDirective`,
3438
[APPLY_DIRECTIVES]: `applyDirectives`,
3539
[RENDER_LIST]: `renderList`,

packages/compiler-core/src/transforms/transformElement.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ import {
2222
APPLY_DIRECTIVES,
2323
RESOLVE_DIRECTIVE,
2424
RESOLVE_COMPONENT,
25+
RESOLVE_DYNAMIC_COMPONENT,
2526
MERGE_PROPS,
2627
TO_HANDLERS,
2728
PORTAL,
2829
SUSPENSE
2930
} from '../runtimeHelpers'
30-
import { getInnerRange, isVSlot, toValidAssetId } from '../utils'
31+
import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils'
3132
import { buildSlots } from './vSlot'
3233
import { isStaticNode } from './hoistStatic'
3334

@@ -55,24 +56,55 @@ export const transformElement: NodeTransform = (node, context) => {
5556
let patchFlag: number = 0
5657
let runtimeDirectives: DirectiveNode[] | undefined
5758
let dynamicPropNames: string[] | undefined
59+
let dynamicComponent: string | CallExpression | undefined
5860

59-
if (isComponent) {
61+
// handle dynamic component
62+
const isProp = findProp(node, 'is')
63+
if (node.tag === 'component') {
64+
if (isProp) {
65+
// static <component is="foo" />
66+
if (isProp.type === NodeTypes.ATTRIBUTE) {
67+
const tag = isProp.value && isProp.value.content
68+
if (tag) {
69+
context.helper(RESOLVE_COMPONENT)
70+
context.components.add(tag)
71+
dynamicComponent = toValidAssetId(tag, `component`)
72+
}
73+
}
74+
// dynamic <component :is="asdf" />
75+
else if (isProp.exp) {
76+
dynamicComponent = createCallExpression(
77+
context.helper(RESOLVE_DYNAMIC_COMPONENT),
78+
[isProp.exp]
79+
)
80+
}
81+
}
82+
}
83+
84+
if (isComponent && !dynamicComponent) {
6085
context.helper(RESOLVE_COMPONENT)
6186
context.components.add(node.tag)
6287
}
6388

6489
const args: CallExpression['arguments'] = [
65-
isComponent
66-
? toValidAssetId(node.tag, `component`)
67-
: node.tagType === ElementTypes.PORTAL
68-
? context.helper(PORTAL)
69-
: node.tagType === ElementTypes.SUSPENSE
70-
? context.helper(SUSPENSE)
71-
: `"${node.tag}"`
90+
dynamicComponent
91+
? dynamicComponent
92+
: isComponent
93+
? toValidAssetId(node.tag, `component`)
94+
: node.tagType === ElementTypes.PORTAL
95+
? context.helper(PORTAL)
96+
: node.tagType === ElementTypes.SUSPENSE
97+
? context.helper(SUSPENSE)
98+
: `"${node.tag}"`
7299
]
73100
// props
74101
if (hasProps) {
75-
const propsBuildResult = buildProps(node, context)
102+
const propsBuildResult = buildProps(
103+
node,
104+
context,
105+
// skip reserved "is" prop <component is>
106+
node.props.filter(p => p !== isProp)
107+
)
76108
patchFlag = propsBuildResult.patchFlag
77109
dynamicPropNames = propsBuildResult.dynamicPropNames
78110
runtimeDirectives = propsBuildResult.directives

packages/runtime-core/__tests__/helpers/resolveAssets.spec.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
resolveComponent,
66
resolveDirective,
77
Component,
8-
Directive
8+
Directive,
9+
resolveDynamicComponent
910
} from '@vue/runtime-test'
1011

1112
describe('resolveAssets', () => {
@@ -90,5 +91,30 @@ describe('resolveAssets', () => {
9091
expect('Failed to resolve component: foo').toHaveBeenWarned()
9192
expect('Failed to resolve directive: bar').toHaveBeenWarned()
9293
})
94+
95+
test('resolve dynamic component', () => {
96+
const app = createApp()
97+
const dynamicComponents = {
98+
foo: () => 'foo',
99+
bar: () => 'bar',
100+
baz: { render: () => 'baz' }
101+
}
102+
let foo, bar, baz // dynamic components
103+
const Root = {
104+
components: { foo: dynamicComponents.foo },
105+
setup() {
106+
return () => {
107+
foo = resolveDynamicComponent('foo') // <component is="foo"/>
108+
bar = resolveDynamicComponent(dynamicComponents.bar) // <component :is="bar"/>, function
109+
baz = resolveDynamicComponent(dynamicComponents.baz) // <component :is="baz"/>, object
110+
}
111+
}
112+
}
113+
const root = nodeOps.createElement('div')
114+
app.mount(Root, root)
115+
expect(foo).toBe(dynamicComponents.foo)
116+
expect(bar).toBe(dynamicComponents.bar)
117+
expect(baz).toBe(dynamicComponents.baz)
118+
})
93119
})
94120
})

packages/runtime-core/src/helpers/resolveAssets.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
import { currentRenderingInstance } from '../componentRenderUtils'
22
import { currentInstance, Component } from '../component'
33
import { Directive } from '../directives'
4-
import { camelize, capitalize } from '@vue/shared'
4+
import {
5+
camelize,
6+
capitalize,
7+
isString,
8+
isObject,
9+
isFunction
10+
} from '@vue/shared'
511
import { warn } from '../warning'
612

713
export function resolveComponent(name: string): Component | undefined {
814
return resolveAsset('components', name)
915
}
1016

17+
export function resolveDynamicComponent(
18+
component: unknown
19+
): Component | undefined {
20+
if (!component) return
21+
if (isString(component)) {
22+
return resolveAsset('components', component)
23+
} else if (isFunction(component) || isObject(component)) {
24+
return component
25+
}
26+
}
27+
1128
export function resolveDirective(name: string): Directive | undefined {
1229
return resolveAsset('directives', name)
1330
}

packages/runtime-core/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export {
3939
// Internal, for compiler generated code
4040
// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
4141
export { applyDirectives } from './directives'
42-
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
42+
export {
43+
resolveComponent,
44+
resolveDirective,
45+
resolveDynamicComponent
46+
} from './helpers/resolveAssets'
4347
export { renderList } from './helpers/renderList'
4448
export { toString } from './helpers/toString'
4549
export { toHandlers } from './helpers/toHandlers'

0 commit comments

Comments
 (0)