fix: dynamic component fallback to native element

fix #870
This commit is contained in:
Evan You 2020-03-23 14:47:04 -04:00
parent 1b2149dbb2
commit f529dbde23
7 changed files with 80 additions and 34 deletions

View File

@ -798,9 +798,18 @@ describe('compiler: element transform', () => {
describe('dynamic component', () => { describe('dynamic component', () => {
test('static binding', () => { test('static binding', () => {
const { node, root } = parseWithBind(`<component is="foo" />`) const { node, root } = parseWithBind(`<component is="foo" />`)
expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT) expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT)
expect(node).toMatchObject({ expect(node).toMatchObject({
tag: '_component_foo' tag: {
callee: RESOLVE_DYNAMIC_COMPONENT,
arguments: [
{
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo',
isStatic: true
}
]
}
}) })
}) })
@ -813,7 +822,8 @@ describe('compiler: element transform', () => {
arguments: [ arguments: [
{ {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: 'foo' content: 'foo',
isStatic: false
} }
] ]
} }

View File

@ -204,19 +204,13 @@ export function resolveComponentType(
// 1. dynamic component // 1. dynamic component
const isProp = node.tag === 'component' && findProp(node, 'is') const isProp = node.tag === 'component' && findProp(node, 'is')
if (isProp) { if (isProp) {
// static <component is="foo" /> const exp =
if (isProp.type === NodeTypes.ATTRIBUTE) { isProp.type === NodeTypes.ATTRIBUTE
const isType = isProp.value && isProp.value.content ? isProp.value && createSimpleExpression(isProp.value.content, true)
if (isType) { : isProp.exp
context.helper(RESOLVE_COMPONENT) if (exp) {
context.components.add(isType)
return toValidAssetId(isType, `component`)
}
}
// dynamic <component :is="asdf" />
else if (isProp.exp) {
return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [ return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
isProp.exp exp
]) ])
} }
} }

View File

@ -20,13 +20,11 @@ describe('ssr: components', () => {
test('dynamic component', () => { test('dynamic component', () => {
expect(compile(`<component is="foo" prop="b" />`).code) expect(compile(`<component is="foo" prop="b" />`).code)
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const { resolveComponent: _resolveComponent, withCtx: _withCtx } = require(\\"vue\\") "const { resolveDynamicComponent: _resolveDynamicComponent, withCtx: _withCtx } = require(\\"vue\\")
const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) { return function ssrRender(_ctx, _push, _parent) {
const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_resolveDynamicComponent(\\"foo\\"), { prop: \\"b\\" }, null, _parent))
_push(_ssrRenderComponent(_component_foo, { prop: \\"b\\" }, null, _parent))
}" }"
`) `)

View File

@ -6,11 +6,14 @@ import {
Component, Component,
Directive, Directive,
resolveDynamicComponent, resolveDynamicComponent,
h h,
serializeInner
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { mockWarn } from '@vue/shared' import { mockWarn } from '@vue/shared'
describe('resolveAssets', () => { describe('resolveAssets', () => {
mockWarn()
test('should work', () => { test('should work', () => {
const FooBar = () => null const FooBar = () => null
const BarBaz = { mounted: () => null } const BarBaz = { mounted: () => null }
@ -63,8 +66,6 @@ describe('resolveAssets', () => {
}) })
describe('warning', () => { describe('warning', () => {
mockWarn()
test('used outside render() or setup()', () => { test('used outside render() or setup()', () => {
resolveComponent('foo') resolveComponent('foo')
expect( expect(
@ -128,5 +129,22 @@ describe('resolveAssets', () => {
expect(bar).toBe(dynamicComponents.bar) expect(bar).toBe(dynamicComponents.bar)
expect(baz).toBe(dynamicComponents.baz) expect(baz).toBe(dynamicComponents.baz)
}) })
test('resolve dynamic component should fallback to plain element without warning', () => {
const Root = {
setup() {
return () => {
return h(resolveDynamicComponent('div') as string, null, {
default: () => 'hello'
})
}
}
}
const app = createApp(Root)
const root = nodeOps.createElement('div')
app.mount(root)
expect(serializeInner(root)).toBe('<div>hello</div>')
})
}) })
}) })

View File

@ -106,7 +106,7 @@ describe('vnode', () => {
const vnode = createVNode('p', null, ['foo']) const vnode = createVNode('p', null, ['foo'])
expect(vnode.children).toMatchObject(['foo']) expect(vnode.children).toMatchObject(['foo'])
expect(vnode.shapeFlag).toBe( expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT + ShapeFlags.ARRAY_CHILDREN ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
) )
}) })
@ -114,7 +114,7 @@ describe('vnode', () => {
const vnode = createVNode('p', null, { foo: 'foo' }) const vnode = createVNode('p', null, { foo: 'foo' })
expect(vnode.children).toMatchObject({ foo: 'foo' }) expect(vnode.children).toMatchObject({ foo: 'foo' })
expect(vnode.shapeFlag).toBe( expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
) )
}) })
@ -122,7 +122,7 @@ describe('vnode', () => {
const vnode = createVNode('p', null, nop) const vnode = createVNode('p', null, nop)
expect(vnode.children).toMatchObject({ default: nop }) expect(vnode.children).toMatchObject({ default: nop })
expect(vnode.shapeFlag).toBe( expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
) )
}) })
@ -130,7 +130,19 @@ describe('vnode', () => {
const vnode = createVNode('p', null, 'foo') const vnode = createVNode('p', null, 'foo')
expect(vnode.children).toBe('foo') expect(vnode.children).toBe('foo')
expect(vnode.shapeFlag).toBe( expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT + ShapeFlags.TEXT_CHILDREN ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
)
})
test('element with slots', () => {
const children = [createVNode('span', null, 'hello')]
const vnode = createVNode('div', null, {
default: () => children
})
expect(vnode.children).toBe(children)
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
) )
}) })
}) })

View File

@ -24,10 +24,14 @@ export function resolveComponent(name: string): Component | undefined {
export function resolveDynamicComponent( export function resolveDynamicComponent(
component: unknown component: unknown
): Component | undefined { ): Component | string | undefined {
if (!component) return if (!component) return
if (isString(component)) { if (isString(component)) {
return resolveAsset(COMPONENTS, component, currentRenderingInstance) return (
resolveAsset(COMPONENTS, component, currentRenderingInstance, false) ||
// fallback to plain element
component
)
} else if (isFunction(component) || isObject(component)) { } else if (isFunction(component) || isObject(component)) {
return component return component
} }
@ -41,7 +45,8 @@ export function resolveDirective(name: string): Directive | undefined {
function resolveAsset( function resolveAsset(
type: typeof COMPONENTS, type: typeof COMPONENTS,
name: string, name: string,
instance?: ComponentInternalInstance | null instance?: ComponentInternalInstance | null,
warnMissing?: boolean
): Component | undefined ): Component | undefined
// overload 2: directives // overload 2: directives
function resolveAsset( function resolveAsset(
@ -54,7 +59,8 @@ function resolveAsset(
type: typeof COMPONENTS | typeof DIRECTIVES, type: typeof COMPONENTS | typeof DIRECTIVES,
name: string, name: string,
instance: ComponentInternalInstance | null = currentRenderingInstance || instance: ComponentInternalInstance | null = currentRenderingInstance ||
currentInstance currentInstance,
warnMissing = true
) { ) {
if (instance) { if (instance) {
let camelized, capitalized let camelized, capitalized
@ -75,7 +81,8 @@ function resolveAsset(
res = self res = self
} }
} }
if (__DEV__ && !res) { if (__DEV__ && warnMissing && !res) {
debugger
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`) warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
} }
return res return res

View File

@ -397,9 +397,16 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
} else if (isArray(children)) { } else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') { } else if (typeof children === 'object') {
type = ShapeFlags.SLOTS_CHILDREN // in case <component :is="x"> resolves to native element, the vnode call
if (!(children as RawSlots)._) { // will receive slots object.
;(children as RawSlots)._ctx = currentRenderingInstance if (vnode.shapeFlag & ShapeFlags.ELEMENT && (children as any).default) {
normalizeChildren(vnode, (children as any).default())
return
} else {
type = ShapeFlags.SLOTS_CHILDREN
if (!(children as RawSlots)._) {
;(children as RawSlots)._ctx = currentRenderingInstance
}
} }
} else if (isFunction(children)) { } else if (isFunction(children)) {
children = { default: children, _ctx: currentRenderingInstance } children = { default: children, _ctx: currentRenderingInstance }