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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user