]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): support keep-alive in templates
authorEvan You <yyx990803@gmail.com>
Tue, 5 Nov 2019 15:26:36 +0000 (10:26 -0500)
committerEvan You <yyx990803@gmail.com>
Tue, 5 Nov 2019 15:26:56 +0000 (10:26 -0500)
packages/compiler-core/__tests__/transforms/transformElement.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/parse.ts
packages/compiler-core/src/runtimeHelpers.ts
packages/compiler-core/src/transforms/transformElement.ts

index 06ed14e54d1565f389ff2f9092e5e371384f10d0..00f5274aba489be4b111762c766b468ea6a3767a 100644 (file)
@@ -8,7 +8,9 @@ import {
   TO_HANDLERS,
   helperNameMap,
   PORTAL,
-  RESOLVE_DYNAMIC_COMPONENT
+  RESOLVE_DYNAMIC_COMPONENT,
+  SUSPENSE,
+  KEEP_ALIVE
 } from '../../src/runtimeHelpers'
 import {
   CallExpression,
@@ -265,50 +267,97 @@ describe('compiler: element transform', () => {
     ])
   })
 
-  test('should handle <portal> element', () => {
-    const { node } = parseWithElementTransform(
-      `<portal target="#foo"><span /></portal>`
-    )
-    expect(node.callee).toBe(CREATE_VNODE)
-    expect(node.arguments).toMatchObject([
-      PORTAL,
-      createObjectMatcher({
-        target: '#foo'
-      }),
-      [
-        {
-          type: NodeTypes.ELEMENT,
-          tag: 'span',
-          codegenNode: {
-            callee: CREATE_VNODE,
-            arguments: [`"span"`]
+  test('should handle <Portal> with normal children', () => {
+    function assert(tag: string) {
+      const { root, node } = parseWithElementTransform(
+        `<${tag} target="#foo"><span /></${tag}>`
+      )
+      expect(root.components.length).toBe(0)
+      expect(root.helpers).toContain(PORTAL)
+      expect(node.callee).toBe(CREATE_VNODE)
+      expect(node.arguments).toMatchObject([
+        PORTAL,
+        createObjectMatcher({
+          target: '#foo'
+        }),
+        [
+          {
+            type: NodeTypes.ELEMENT,
+            tag: 'span',
+            codegenNode: {
+              callee: CREATE_VNODE,
+              arguments: [`"span"`]
+            }
           }
-        }
-      ]
-    ])
+        ]
+      ])
+    }
+
+    assert(`portal`)
+    assert(`Portal`)
   })
 
-  test('should handle <Portal> element', () => {
-    const { node } = parseWithElementTransform(
-      `<Portal target="#foo"><span /></Portal>`
+  test('should handle <Suspense>', () => {
+    function assert(tag: string, content: string, hasFallback?: boolean) {
+      const { root, node } = parseWithElementTransform(
+        `<${tag}>${content}</${tag}>`
+      )
+      expect(root.components.length).toBe(0)
+      expect(root.helpers).toContain(SUSPENSE)
+      expect(node.callee).toBe(CREATE_VNODE)
+      expect(node.arguments).toMatchObject([
+        SUSPENSE,
+        `null`,
+        hasFallback
+          ? createObjectMatcher({
+              default: {
+                type: NodeTypes.JS_FUNCTION_EXPRESSION
+              },
+              fallback: {
+                type: NodeTypes.JS_FUNCTION_EXPRESSION
+              },
+              _compiled: `[true]`
+            })
+          : createObjectMatcher({
+              default: {
+                type: NodeTypes.JS_FUNCTION_EXPRESSION
+              },
+              _compiled: `[true]`
+            })
+      ])
+    }
+
+    assert(`suspense`, `foo`)
+    assert(`suspense`, `<template #default>foo</template>`)
+    assert(
+      `suspense`,
+      `<template #default>foo</template><template #fallback>fallback</template>`,
+      true
     )
-    expect(node.callee).toBe(CREATE_VNODE)
-    expect(node.arguments).toMatchObject([
-      PORTAL,
-      createObjectMatcher({
-        target: '#foo'
-      }),
-      [
-        {
-          type: NodeTypes.ELEMENT,
-          tag: 'span',
-          codegenNode: {
-            callee: CREATE_VNODE,
-            arguments: [`"span"`]
-          }
-        }
-      ]
-    ])
+  })
+
+  test('should handle <KeepAlive>', () => {
+    function assert(tag: string) {
+      const { root, node } = parseWithElementTransform(
+        `<${tag}><span /></${tag}>`
+      )
+      expect(root.components.length).toBe(0)
+      expect(root.helpers).toContain(KEEP_ALIVE)
+      expect(node.callee).toBe(CREATE_VNODE)
+      expect(node.arguments).toMatchObject([
+        KEEP_ALIVE,
+        `null`,
+        createObjectMatcher({
+          default: {
+            type: NodeTypes.JS_FUNCTION_EXPRESSION
+          },
+          _compiled: `[true]`
+        })
+      ])
+    }
+
+    assert(`keep-alive`)
+    assert(`KeepAlive`)
   })
 
   test('error on v-bind with no argument', () => {
index 9ba06608640dc381db669dfe42ce0296374eb361..ebd7a605c83e2a2b47579742eb3e63ddb3404a18 100644 (file)
@@ -51,9 +51,7 @@ export const enum ElementTypes {
   ELEMENT,
   COMPONENT,
   SLOT,
-  TEMPLATE,
-  PORTAL,
-  SUSPENSE
+  TEMPLATE
 }
 
 export interface Node {
@@ -105,8 +103,6 @@ export type ElementNode =
   | ComponentNode
   | SlotOutletNode
   | TemplateNode
-  | PortalNode
-  | SuspenseNode
 
 export interface BaseElementNode extends Node {
   type: NodeTypes.ELEMENT
@@ -147,16 +143,6 @@ export interface TemplateNode extends BaseElementNode {
   codegenNode: ElementCodegenNode | undefined | CacheExpression
 }
 
-export interface PortalNode extends BaseElementNode {
-  tagType: ElementTypes.PORTAL
-  codegenNode: ElementCodegenNode | undefined | CacheExpression
-}
-
-export interface SuspenseNode extends BaseElementNode {
-  tagType: ElementTypes.SUSPENSE
-  codegenNode: ElementCodegenNode | undefined | CacheExpression
-}
-
 export interface TextNode extends Node {
   type: NodeTypes.TEXT
   content: string
index 4b2cec420de04744d3823bfd3e5da0ee5579e62f..57f21680f324c188380c52cfe5a56f8ddbd06b48 100644 (file)
@@ -1,4 +1,4 @@
-import { NO } from '@vue/shared'
+import { NO, makeMap } from '@vue/shared'
 import {
   ErrorCodes,
   createCompilerError,
@@ -29,6 +29,12 @@ import {
 } from './ast'
 import { extend } from '@vue/shared'
 
+// Portal and Fragment are native types, not components
+const isBuiltInComponent = /*#__PURE__*/ makeMap(
+  `suspense,keep-alive,keepalive,transition`,
+  true
+)
+
 export interface ParserOptions {
   isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
   isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex
@@ -467,15 +473,15 @@ function parseTag(
   if (!context.inPre && !context.options.isCustomElement(tag)) {
     if (context.options.isNativeTag) {
       if (!context.options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
-    } else {
-      if (/^[A-Z]/.test(tag)) tagType = ElementTypes.COMPONENT
+    } else if (isBuiltInComponent(tag) || /^[A-Z]/.test(tag)) {
+      tagType = ElementTypes.COMPONENT
     }
 
-    if (tag === 'slot') tagType = ElementTypes.SLOT
-    else if (tag === 'template') tagType = ElementTypes.TEMPLATE
-    else if (tag === 'portal' || tag === 'Portal') tagType = ElementTypes.PORTAL
-    else if (tag === 'suspense' || tag === 'Suspense')
-      tagType = ElementTypes.SUSPENSE
+    if (tag === 'slot') {
+      tagType = ElementTypes.SLOT
+    } else if (tag === 'template') {
+      tagType = ElementTypes.TEMPLATE
+    }
   }
 
   return {
index f696316868809fcd1f1281dcf5025e5d061b059a..a6d679ef9d262c82eddef79cd28d909316f0574f 100644 (file)
@@ -1,6 +1,7 @@
 export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``)
 export const PORTAL = Symbol(__DEV__ ? `Portal` : ``)
 export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``)
+export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``)
 export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``)
 export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
 export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
@@ -28,6 +29,7 @@ export const helperNameMap: any = {
   [FRAGMENT]: `Fragment`,
   [PORTAL]: `Portal`,
   [SUSPENSE]: `Suspense`,
+  [KEEP_ALIVE]: `KeepAlive`,
   [OPEN_BLOCK]: `openBlock`,
   [CREATE_BLOCK]: `createBlock`,
   [CREATE_VNODE]: `createVNode`,
index f348c8ce1fcdebd783c94c3f5bd609e20fcfe604..909b460ffc25ca6f94ea6197a89b111fd2e3a325 100644 (file)
@@ -26,7 +26,8 @@ import {
   MERGE_PROPS,
   TO_HANDLERS,
   PORTAL,
-  SUSPENSE
+  SUSPENSE,
+  KEEP_ALIVE
 } from '../runtimeHelpers'
 import { getInnerRange, isVSlot, toValidAssetId, findProp } from '../utils'
 import { buildSlots } from './vSlot'
@@ -51,8 +52,13 @@ export const transformElement: NodeTransform = (node, context) => {
   // perform the work on exit, after all child expressions have been
   // processed and merged.
   return () => {
-    const isComponent = node.tagType === ElementTypes.COMPONENT
-    let hasProps = node.props.length > 0
+    const { tag, tagType, props } = node
+    const isPortal = tag === 'portal' || tag === 'Portal'
+    const isSuspense = tag === 'suspense' || tag === 'Suspense'
+    const isKeepAlive = tag === 'keep-alive' || tag === 'KeepAlive'
+    const isComponent = tagType === ElementTypes.COMPONENT
+
+    let hasProps = props.length > 0
     let patchFlag: number = 0
     let runtimeDirectives: DirectiveNode[] | undefined
     let dynamicPropNames: string[] | undefined
@@ -60,7 +66,7 @@ export const transformElement: NodeTransform = (node, context) => {
 
     // handle dynamic component
     const isProp = findProp(node, 'is')
-    if (node.tag === 'component') {
+    if (tag === 'component') {
       if (isProp) {
         // static <component is="foo" />
         if (isProp.type === NodeTypes.ATTRIBUTE) {
@@ -81,22 +87,26 @@ export const transformElement: NodeTransform = (node, context) => {
       }
     }
 
-    if (isComponent && !dynamicComponent) {
+    let nodeType
+    if (dynamicComponent) {
+      nodeType = dynamicComponent
+    } else if (isPortal) {
+      nodeType = context.helper(PORTAL)
+    } else if (isSuspense) {
+      nodeType = context.helper(SUSPENSE)
+    } else if (isKeepAlive) {
+      nodeType = context.helper(KEEP_ALIVE)
+    } else if (isComponent) {
+      // user component w/ resolve
       context.helper(RESOLVE_COMPONENT)
-      context.components.add(node.tag)
+      context.components.add(tag)
+      nodeType = toValidAssetId(tag, `component`)
+    } else {
+      // plain element
+      nodeType = `"${node.tag}"`
     }
 
-    const args: CallExpression['arguments'] = [
-      dynamicComponent
-        ? dynamicComponent
-        : isComponent
-          ? toValidAssetId(node.tag, `component`)
-          : node.tagType === ElementTypes.PORTAL
-            ? context.helper(PORTAL)
-            : node.tagType === ElementTypes.SUSPENSE
-              ? context.helper(SUSPENSE)
-              : `"${node.tag}"`
-    ]
+    const args: CallExpression['arguments'] = [nodeType]
     // props
     if (hasProps) {
       const propsBuildResult = buildProps(
@@ -120,7 +130,8 @@ export const transformElement: NodeTransform = (node, context) => {
       if (!hasProps) {
         args.push(`null`)
       }
-      if (isComponent || node.tagType === ElementTypes.SUSPENSE) {
+      // Portal should have normal children instead of slots
+      if (isComponent && !isPortal) {
         const { slots, hasDynamicSlots } = buildSlots(node, context)
         args.push(slots)
         if (hasDynamicSlots) {