Skip to content

Commit f529dbd

Browse files
committed
fix: dynamic component fallback to native element
fix #870
1 parent 1b2149d commit f529dbd

File tree

7 files changed

+80
-34
lines changed

7 files changed

+80
-34
lines changed

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

+13-3
Original file line numberDiff line numberDiff line change
@@ -798,9 +798,18 @@ describe('compiler: element transform', () => {
798798
describe('dynamic component', () => {
799799
test('static binding', () => {
800800
const { node, root } = parseWithBind(`<component is="foo" />`)
801-
expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT)
801+
expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
802802
expect(node).toMatchObject({
803-
tag: '_component_foo'
803+
tag: {
804+
callee: RESOLVE_DYNAMIC_COMPONENT,
805+
arguments: [
806+
{
807+
type: NodeTypes.SIMPLE_EXPRESSION,
808+
content: 'foo',
809+
isStatic: true
810+
}
811+
]
812+
}
804813
})
805814
})
806815

@@ -813,7 +822,8 @@ describe('compiler: element transform', () => {
813822
arguments: [
814823
{
815824
type: NodeTypes.SIMPLE_EXPRESSION,
816-
content: 'foo'
825+
content: 'foo',
826+
isStatic: false
817827
}
818828
]
819829
}

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

+6-12
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,13 @@ export function resolveComponentType(
204204
// 1. dynamic component
205205
const isProp = node.tag === 'component' && findProp(node, 'is')
206206
if (isProp) {
207-
// static <component is="foo" />
208-
if (isProp.type === NodeTypes.ATTRIBUTE) {
209-
const isType = isProp.value && isProp.value.content
210-
if (isType) {
211-
context.helper(RESOLVE_COMPONENT)
212-
context.components.add(isType)
213-
return toValidAssetId(isType, `component`)
214-
}
215-
}
216-
// dynamic <component :is="asdf" />
217-
else if (isProp.exp) {
207+
const exp =
208+
isProp.type === NodeTypes.ATTRIBUTE
209+
? isProp.value && createSimpleExpression(isProp.value.content, true)
210+
: isProp.exp
211+
if (exp) {
218212
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
219-
isProp.exp
213+
exp
220214
])
221215
}
222216
}

packages/compiler-ssr/__tests__/ssrComponent.spec.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@ describe('ssr: components', () => {
2020
test('dynamic component', () => {
2121
expect(compile(`<component is="foo" prop="b" />`).code)
2222
.toMatchInlineSnapshot(`
23-
"const { resolveComponent: _resolveComponent, withCtx: _withCtx } = require(\\"vue\\")
23+
"const { resolveDynamicComponent: _resolveDynamicComponent, withCtx: _withCtx } = require(\\"vue\\")
2424
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
2525
2626
return function ssrRender(_ctx, _push, _parent) {
27-
const _component_foo = _resolveComponent(\\"foo\\")
28-
29-
_push(_ssrRenderComponent(_component_foo, { prop: \\"b\\" }, null, _parent))
27+
_push(_ssrRenderComponent(_resolveDynamicComponent(\\"foo\\"), { prop: \\"b\\" }, null, _parent))
3028
}"
3129
`)
3230

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

+21-3
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import {
66
Component,
77
Directive,
88
resolveDynamicComponent,
9-
h
9+
h,
10+
serializeInner
1011
} from '@vue/runtime-test'
1112
import { mockWarn } from '@vue/shared'
1213

1314
describe('resolveAssets', () => {
15+
mockWarn()
16+
1417
test('should work', () => {
1518
const FooBar = () => null
1619
const BarBaz = { mounted: () => null }
@@ -63,8 +66,6 @@ describe('resolveAssets', () => {
6366
})
6467

6568
describe('warning', () => {
66-
mockWarn()
67-
6869
test('used outside render() or setup()', () => {
6970
resolveComponent('foo')
7071
expect(
@@ -128,5 +129,22 @@ describe('resolveAssets', () => {
128129
expect(bar).toBe(dynamicComponents.bar)
129130
expect(baz).toBe(dynamicComponents.baz)
130131
})
132+
133+
test('resolve dynamic component should fallback to plain element without warning', () => {
134+
const Root = {
135+
setup() {
136+
return () => {
137+
return h(resolveDynamicComponent('div') as string, null, {
138+
default: () => 'hello'
139+
})
140+
}
141+
}
142+
}
143+
144+
const app = createApp(Root)
145+
const root = nodeOps.createElement('div')
146+
app.mount(root)
147+
expect(serializeInner(root)).toBe('<div>hello</div>')
148+
})
131149
})
132150
})

packages/runtime-core/__tests__/vnode.spec.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -106,31 +106,43 @@ describe('vnode', () => {
106106
const vnode = createVNode('p', null, ['foo'])
107107
expect(vnode.children).toMatchObject(['foo'])
108108
expect(vnode.shapeFlag).toBe(
109-
ShapeFlags.ELEMENT + ShapeFlags.ARRAY_CHILDREN
109+
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
110110
)
111111
})
112112

113113
test('object', () => {
114114
const vnode = createVNode('p', null, { foo: 'foo' })
115115
expect(vnode.children).toMatchObject({ foo: 'foo' })
116116
expect(vnode.shapeFlag).toBe(
117-
ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN
117+
ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
118118
)
119119
})
120120

121121
test('function', () => {
122122
const vnode = createVNode('p', null, nop)
123123
expect(vnode.children).toMatchObject({ default: nop })
124124
expect(vnode.shapeFlag).toBe(
125-
ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN
125+
ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
126126
)
127127
})
128128

129129
test('string', () => {
130130
const vnode = createVNode('p', null, 'foo')
131131
expect(vnode.children).toBe('foo')
132132
expect(vnode.shapeFlag).toBe(
133-
ShapeFlags.ELEMENT + ShapeFlags.TEXT_CHILDREN
133+
ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
134+
)
135+
})
136+
137+
test('element with slots', () => {
138+
const children = [createVNode('span', null, 'hello')]
139+
const vnode = createVNode('div', null, {
140+
default: () => children
141+
})
142+
143+
expect(vnode.children).toBe(children)
144+
expect(vnode.shapeFlag).toBe(
145+
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
134146
)
135147
})
136148
})

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

+12-5
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ export function resolveComponent(name: string): Component | undefined {
2424

2525
export function resolveDynamicComponent(
2626
component: unknown
27-
): Component | undefined {
27+
): Component | string | undefined {
2828
if (!component) return
2929
if (isString(component)) {
30-
return resolveAsset(COMPONENTS, component, currentRenderingInstance)
30+
return (
31+
resolveAsset(COMPONENTS, component, currentRenderingInstance, false) ||
32+
// fallback to plain element
33+
component
34+
)
3135
} else if (isFunction(component) || isObject(component)) {
3236
return component
3337
}
@@ -41,7 +45,8 @@ export function resolveDirective(name: string): Directive | undefined {
4145
function resolveAsset(
4246
type: typeof COMPONENTS,
4347
name: string,
44-
instance?: ComponentInternalInstance | null
48+
instance?: ComponentInternalInstance | null,
49+
warnMissing?: boolean
4550
): Component | undefined
4651
// overload 2: directives
4752
function resolveAsset(
@@ -54,7 +59,8 @@ function resolveAsset(
5459
type: typeof COMPONENTS | typeof DIRECTIVES,
5560
name: string,
5661
instance: ComponentInternalInstance | null = currentRenderingInstance ||
57-
currentInstance
62+
currentInstance,
63+
warnMissing = true
5864
) {
5965
if (instance) {
6066
let camelized, capitalized
@@ -75,7 +81,8 @@ function resolveAsset(
7581
res = self
7682
}
7783
}
78-
if (__DEV__ && !res) {
84+
if (__DEV__ && warnMissing && !res) {
85+
debugger
7986
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
8087
}
8188
return res

packages/runtime-core/src/vnode.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,16 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
397397
} else if (isArray(children)) {
398398
type = ShapeFlags.ARRAY_CHILDREN
399399
} else if (typeof children === 'object') {
400-
type = ShapeFlags.SLOTS_CHILDREN
401-
if (!(children as RawSlots)._) {
402-
;(children as RawSlots)._ctx = currentRenderingInstance
400+
// in case <component :is="x"> resolves to native element, the vnode call
401+
// will receive slots object.
402+
if (vnode.shapeFlag & ShapeFlags.ELEMENT && (children as any).default) {
403+
normalizeChildren(vnode, (children as any).default())
404+
return
405+
} else {
406+
type = ShapeFlags.SLOTS_CHILDREN
407+
if (!(children as RawSlots)._) {
408+
;(children as RawSlots)._ctx = currentRenderingInstance
409+
}
403410
}
404411
} else if (isFunction(children)) {
405412
children = { default: children, _ctx: currentRenderingInstance }

0 commit comments

Comments
 (0)