]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(core): support dynamic component via <component :is> (#320)
author宋铄运 <fnlctrl@gmail.com>
Fri, 18 Oct 2019 16:09:04 +0000 (00:09 +0800)
committerEvan You <yyx990803@gmail.com>
Fri, 18 Oct 2019 16:09:04 +0000 (12:09 -0400)
packages/compiler-core/__tests__/transforms/transformElement.spec.ts
packages/compiler-core/src/runtimeHelpers.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/runtime-core/__tests__/helpers/resolveAssets.spec.ts
packages/runtime-core/src/helpers/resolveAssets.ts
packages/runtime-core/src/index.ts

index 8f803640a80cc1db9fee771debd2dc72a4782fdb..6bda8342bd5630914495708dacc05819a9633d33 100644 (file)
@@ -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'
+            }
+          ]
+        }
+      ])
+    })
+  })
 })
index 911a7ebbb20b0088fa8baa375cc609f7667ba3d8..8993d067494c6f591ab230bca652829d0b78af17 100644 (file)
@@ -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`,
index 74ef7f55b5bbea69d9154d15fa2d70bc1ab44131..88838886dd7b1f02a222e02658bc9eb318eec4ca 100644 (file)
@@ -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
index 77d61621298d74acad816445bde523e90fc1a470..7431c6011ab51f8752fae810a6539789a50ef739 100644 (file)
@@ -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)
+    })
   })
 })
index b129d552f658df820683bb6aa918aefc64e65d95..91283f0f60e5b3532e2f716db7fe34cb30ec89f4 100644 (file)
@@ -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)
 }
index 3e4abf82b1b5cba2569e9b10d3e9dd2be9f302d0..75ed1765db7cfec0f083545ae8e50f6622b242b5 100644 (file)
@@ -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'