]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): support v-for on named slots
authorEvan You <yyx990803@gmail.com>
Thu, 3 Oct 2019 03:10:41 +0000 (23:10 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 3 Oct 2019 03:10:41 +0000 (23:10 -0400)
18 files changed:
packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-core/__tests__/testUtils.ts
packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
packages/compiler-core/__tests__/transforms/vSlot.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/runtimeConstants.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-core/src/transforms/vSlot.ts
packages/compiler-core/src/utils.ts
packages/runtime-core/src/helpers/createSlots.ts [new file with mode: 0644]
packages/runtime-core/src/index.ts

index c859f8b08be156c64a69c2a72b6f869f80c37af3..d9ade929bac76119fd196c988bde9d352cbb3b00 100644 (file)
@@ -14,9 +14,7 @@ return function render() {
       _toString(world.burn()),
       (_openBlock(), ok
         ? _createBlock(\\"div\\", { key: 0 }, \\"yes\\")
-        : _createBlock(_Fragment, { key: 1 }, [
-            \\"no\\"
-          ])),
+        : _createBlock(_Fragment, { key: 1 }, [\\"no\\"])),
       (_openBlock(), _createBlock(_Fragment, null, _renderList(list, (value, index) => {
         return (_openBlock(), _createBlock(\\"div\\", null, [
           _createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)
@@ -39,9 +37,7 @@ return function render() {
     toString(_ctx.world.burn()),
     (openBlock(), (_ctx.ok)
       ? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
-      : createBlock(Fragment, { key: 1 }, [
-          \\"no\\"
-        ])),
+      : createBlock(Fragment, { key: 1 }, [\\"no\\"])),
     (openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
       return (openBlock(), createBlock(\\"div\\", null, [
         createVNode(\\"span\\", null, toString(value + index), 1 /* TEXT */)
@@ -63,9 +59,7 @@ export default function render() {
     _toString(_ctx.world.burn()),
     (openBlock(), (_ctx.ok)
       ? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
-      : createBlock(Fragment, { key: 1 }, [
-          \\"no\\"
-        ])),
+      : createBlock(Fragment, { key: 1 }, [\\"no\\"])),
     (openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
       return (openBlock(), createBlock(\\"div\\", null, [
         createVNode(\\"span\\", null, _toString(value + index), 1 /* TEXT */)
index f8c02f8e3d16100401c9494b08bebba1e17bef58..cc061d87de700a6b2a2a50d7d36fc7e5d25dc042 100644 (file)
@@ -7,6 +7,7 @@ import {
   ElementTypes
 } from '../src'
 import { CREATE_VNODE } from '../src/runtimeConstants'
+import { isString } from '@vue/shared'
 
 const leadingBracketRE = /^\[/
 const bracketsRE = /^\[|\]$/g
@@ -26,11 +27,13 @@ export function createObjectMatcher(obj: any) {
         content: key.replace(bracketsRE, ''),
         isStatic: !leadingBracketRE.test(key)
       },
-      value: {
-        type: NodeTypes.SIMPLE_EXPRESSION,
-        content: obj[key].replace(bracketsRE, ''),
-        isStatic: !leadingBracketRE.test(obj[key])
-      }
+      value: isString(obj[key])
+        ? {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: obj[key].replace(bracketsRE, ''),
+            isStatic: !leadingBracketRE.test(obj[key])
+          }
+        : obj[key]
     }))
   }
 }
index b44c2eaefaafd3757f3ce27a8699055c0df35c42..3291e8dc2f588775f0d64f69fe83851b2763c9f1 100644 (file)
@@ -71,9 +71,7 @@ return function render() {
       ? _createBlock(\\"div\\", { key: 0 })
       : orNot
         ? _createBlock(\\"p\\", { key: 1 })
-        : _createBlock(_Fragment, { key: 2 }, [
-            \\"fine\\"
-          ]))
+        : _createBlock(_Fragment, { key: 2 }, [\\"fine\\"]))
   }
 }"
 `;
index a43309a54c4779e3345e059123413af6ae159179..78142174a72eb79ff53503db4c364f81644510a7 100644 (file)
@@ -8,14 +8,8 @@ return function render() {
   const _component_Comp = resolveComponent(\\"Comp\\")
   
   return (openBlock(), createBlock(_component_Comp, null, {
-    [_ctx.one]: ({ foo }) => [
-      toString(foo),
-      toString(_ctx.bar)
-    ],
-    [_ctx.two]: ({ bar }) => [
-      toString(_ctx.foo),
-      toString(bar)
-    ]
+    [_ctx.one]: ({ foo }) => [toString(foo), toString(_ctx.bar)],
+    [_ctx.two]: ({ bar }) => [toString(_ctx.foo), toString(bar)]
   }, 256 /* DYNAMIC_SLOTS */))
 }"
 `;
@@ -28,10 +22,7 @@ return function render() {
   const _component_Comp = resolveComponent(\\"Comp\\")
   
   return (openBlock(), createBlock(_component_Comp, null, {
-    default: ({ foo }) => [
-      toString(foo),
-      toString(_ctx.bar)
-    ]
+    default: ({ foo }) => [toString(foo), toString(_ctx.bar)]
   }))
 }"
 `;
@@ -51,6 +42,92 @@ return function render() {
 }"
 `;
 
+exports[`compiler: transform component slots named slot with v-for w/ prefixIdentifiers: true 1`] = `
+"const { toString, resolveComponent, renderList, createSlots, createVNode, openBlock, createBlock } = Vue
+
+return function render() {
+  const _ctx = this
+  const _component_Comp = resolveComponent(\\"Comp\\")
+  
+  return (openBlock(), createBlock(_component_Comp, null, createSlots({}, [
+    renderList(_ctx.list, (name) => {
+      return {
+        name: name,
+        fn: () => [toString(name)]
+      }
+    })
+  ]), 256 /* DYNAMIC_SLOTS */))
+}"
+`;
+
+exports[`compiler: transform component slots named slot with v-if + prefixIdentifiers: true 1`] = `
+"const { toString, resolveComponent, createSlots, createVNode, openBlock, createBlock } = Vue
+
+return function render() {
+  const _ctx = this
+  const _component_Comp = resolveComponent(\\"Comp\\")
+  
+  return (openBlock(), createBlock(_component_Comp, null, createSlots({}, [
+    (_ctx.ok)
+      ? {
+          name: \\"one\\",
+          fn: (props) => [toString(props)]
+        }
+      : undefined
+  ]), 256 /* DYNAMIC_SLOTS */))
+}"
+`;
+
+exports[`compiler: transform component slots named slot with v-if + v-else-if + v-else 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    const _component_Comp = _resolveComponent(\\"Comp\\")
+    
+    return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({}, [
+      ok
+        ? {
+            name: \\"one\\",
+            fn: () => [\\"foo\\"]
+          }
+        : orNot
+          ? {
+              name: \\"two\\",
+              fn: (props) => [\\"bar\\"]
+            }
+          : {
+              name: \\"one\\",
+              fn: () => [\\"baz\\"]
+            }
+    ]), 256 /* DYNAMIC_SLOTS */))
+  }
+}"
+`;
+
+exports[`compiler: transform component slots named slot with v-if 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { resolveComponent: _resolveComponent, createSlots: _createSlots, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    const _component_Comp = _resolveComponent(\\"Comp\\")
+    
+    return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({}, [
+      ok
+        ? {
+            name: \\"one\\",
+            fn: () => [\\"hello\\"]
+          }
+        : undefined
+    ]), 256 /* DYNAMIC_SLOTS */))
+  }
+}"
+`;
+
 exports[`compiler: transform component slots named slots 1`] = `
 "const { toString, resolveComponent, createVNode, openBlock, createBlock } = Vue
 
@@ -59,14 +136,8 @@ return function render() {
   const _component_Comp = resolveComponent(\\"Comp\\")
   
   return (openBlock(), createBlock(_component_Comp, null, {
-    one: ({ foo }) => [
-      toString(foo),
-      toString(_ctx.bar)
-    ],
-    two: ({ bar }) => [
-      toString(_ctx.foo),
-      toString(bar)
-    ]
+    one: ({ foo }) => [toString(foo), toString(_ctx.bar)],
+    two: ({ bar }) => [toString(_ctx.foo), toString(bar)]
   }))
 }"
 `;
@@ -82,11 +153,7 @@ return function render() {
   return (openBlock(), createBlock(_component_Comp, null, {
     default: ({ foo }) => [
       createVNode(_component_Inner, null, {
-        default: ({ bar }) => [
-          toString(foo),
-          toString(bar),
-          toString(_ctx.baz)
-        ]
+        default: ({ bar }) => [toString(foo), toString(bar), toString(_ctx.baz)]
       }),
       toString(foo),
       toString(_ctx.bar),
index f2f2ad817d8c2417d647b30e3e7c00aecde5f6ec..a36e5388ad140875a541dcc7a6e358aefdc0c718 100644 (file)
@@ -11,14 +11,20 @@ import { transformElement } from '../../src/transforms/transformElement'
 import { transformOn } from '../../src/transforms/vOn'
 import { transformBind } from '../../src/transforms/vBind'
 import { transformExpression } from '../../src/transforms/transformExpression'
-import { trackSlotScopes } from '../../src/transforms/vSlot'
+import {
+  trackSlotScopes,
+  trackVForSlotScopes
+} from '../../src/transforms/vSlot'
+import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeConstants'
+import { createObjectMatcher } from '../testUtils'
+import { PatchFlags } from '@vue/shared'
 
 function parseWithSlots(template: string, options: CompilerOptions = {}) {
   const ast = parse(template)
   transform(ast, {
     nodeTransforms: [
       ...(options.prefixIdentifiers
-        ? [transformExpression, trackSlotScopes]
+        ? [trackVForSlotScopes, transformExpression, trackSlotScopes]
         : []),
       transformElement
     ],
@@ -314,118 +320,311 @@ describe('compiler: transform component slots', () => {
     expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
   })
 
-  test('error on extraneous children w/ named slots', () => {
-    const onError = jest.fn()
-    const source = `<Comp><template #default>foo</template>bar</Comp>`
-    parseWithSlots(source, { onError })
-    const index = source.indexOf('bar')
-    expect(onError.mock.calls[0][0]).toMatchObject({
-      code: ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
-      loc: {
-        source: `bar`,
-        start: {
-          offset: index,
-          line: 1,
-          column: index + 1
-        },
-        end: {
-          offset: index + 3,
-          line: 1,
-          column: index + 4
+  test('named slot with v-if', () => {
+    const { root, slots } = parseWithSlots(
+      `<Comp>
+        <template #one v-if="ok">hello</template>
+      </Comp>`
+    )
+    expect(slots).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${CREATE_SLOTS}`,
+      arguments: [
+        createObjectMatcher({}),
+        {
+          type: NodeTypes.JS_ARRAY_EXPRESSION,
+          elements: [
+            {
+              type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
+              test: { content: `ok` },
+              consequent: createObjectMatcher({
+                name: `one`,
+                fn: {
+                  type: NodeTypes.JS_FUNCTION_EXPRESSION,
+                  returns: [{ type: NodeTypes.TEXT, content: `hello` }]
+                }
+              }),
+              alternate: {
+                content: `undefined`,
+                isStatic: false
+              }
+            }
+          ]
         }
-      }
+      ]
     })
+    expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
+      PatchFlags.DYNAMIC_SLOTS + ''
+    )
+    expect(generate(root).code).toMatchSnapshot()
   })
 
-  test('error on duplicated slot names', () => {
-    const onError = jest.fn()
-    const source = `<Comp><template #foo></template><template #foo></template></Comp>`
-    parseWithSlots(source, { onError })
-    const index = source.lastIndexOf('#foo')
-    expect(onError.mock.calls[0][0]).toMatchObject({
-      code: ErrorCodes.X_DUPLICATE_SLOT_NAMES,
-      loc: {
-        source: `#foo`,
-        start: {
-          offset: index,
-          line: 1,
-          column: index + 1
-        },
-        end: {
-          offset: index + 4,
-          line: 1,
-          column: index + 5
+  test('named slot with v-if + prefixIdentifiers: true', () => {
+    const { root, slots } = parseWithSlots(
+      `<Comp>
+        <template #one="props" v-if="ok">{{ props }}</template>
+      </Comp>`,
+      { prefixIdentifiers: true }
+    )
+    expect(slots).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: CREATE_SLOTS,
+      arguments: [
+        createObjectMatcher({}),
+        {
+          type: NodeTypes.JS_ARRAY_EXPRESSION,
+          elements: [
+            {
+              type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
+              test: { content: `_ctx.ok` },
+              consequent: createObjectMatcher({
+                name: `one`,
+                fn: {
+                  type: NodeTypes.JS_FUNCTION_EXPRESSION,
+                  params: { content: `props` },
+                  returns: [
+                    {
+                      type: NodeTypes.INTERPOLATION,
+                      content: { content: `props` }
+                    }
+                  ]
+                }
+              }),
+              alternate: {
+                content: `undefined`,
+                isStatic: false
+              }
+            }
+          ]
         }
-      }
+      ]
     })
+    expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
+      PatchFlags.DYNAMIC_SLOTS + ''
+    )
+    expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
   })
 
-  test('error on invalid mixed slot usage', () => {
-    const onError = jest.fn()
-    const source = `<Comp v-slot="foo"><template #foo></template></Comp>`
-    parseWithSlots(source, { onError })
-    const index = source.lastIndexOf('#foo')
-    expect(onError.mock.calls[0][0]).toMatchObject({
-      code: ErrorCodes.X_MIXED_SLOT_USAGE,
-      loc: {
-        source: `#foo`,
-        start: {
-          offset: index,
-          line: 1,
-          column: index + 1
-        },
-        end: {
-          offset: index + 4,
-          line: 1,
-          column: index + 5
+  test('named slot with v-if + v-else-if + v-else', () => {
+    const { root, slots } = parseWithSlots(
+      `<Comp>
+        <template #one v-if="ok">foo</template>
+        <template #two="props" v-else-if="orNot">bar</template>
+        <template #one v-else>baz</template>
+      </Comp>`
+    )
+    expect(slots).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: `_${CREATE_SLOTS}`,
+      arguments: [
+        createObjectMatcher({}),
+        {
+          type: NodeTypes.JS_ARRAY_EXPRESSION,
+          elements: [
+            {
+              type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
+              test: { content: `ok` },
+              consequent: createObjectMatcher({
+                name: `one`,
+                fn: {
+                  type: NodeTypes.JS_FUNCTION_EXPRESSION,
+                  params: undefined,
+                  returns: [{ type: NodeTypes.TEXT, content: `foo` }]
+                }
+              }),
+              alternate: {
+                type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
+                test: { content: `orNot` },
+                consequent: createObjectMatcher({
+                  name: `two`,
+                  fn: {
+                    type: NodeTypes.JS_FUNCTION_EXPRESSION,
+                    params: { content: `props` },
+                    returns: [{ type: NodeTypes.TEXT, content: `bar` }]
+                  }
+                }),
+                alternate: createObjectMatcher({
+                  name: `one`,
+                  fn: {
+                    type: NodeTypes.JS_FUNCTION_EXPRESSION,
+                    params: undefined,
+                    returns: [{ type: NodeTypes.TEXT, content: `baz` }]
+                  }
+                })
+              }
+            }
+          ]
         }
-      }
+      ]
     })
+    expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
+      PatchFlags.DYNAMIC_SLOTS + ''
+    )
+    expect(generate(root).code).toMatchSnapshot()
   })
 
-  test('error on v-slot usage on plain elements', () => {
-    const onError = jest.fn()
-    const source = `<div v-slot/>`
-    parseWithSlots(source, { onError })
-    const index = source.indexOf('v-slot')
-    expect(onError.mock.calls[0][0]).toMatchObject({
-      code: ErrorCodes.X_MISPLACED_V_SLOT,
-      loc: {
-        source: `v-slot`,
-        start: {
-          offset: index,
-          line: 1,
-          column: index + 1
-        },
-        end: {
-          offset: index + 6,
-          line: 1,
-          column: index + 7
+  test('named slot with v-for w/ prefixIdentifiers: true', () => {
+    const { root, slots } = parseWithSlots(
+      `<Comp>
+        <template v-for="name in list" #[name]>{{ name }}</template>
+      </Comp>`,
+      { prefixIdentifiers: true }
+    )
+    expect(slots).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: CREATE_SLOTS,
+      arguments: [
+        createObjectMatcher({}),
+        {
+          type: NodeTypes.JS_ARRAY_EXPRESSION,
+          elements: [
+            {
+              type: NodeTypes.JS_CALL_EXPRESSION,
+              callee: RENDER_LIST,
+              arguments: [
+                { content: `_ctx.list` },
+                {
+                  type: NodeTypes.JS_FUNCTION_EXPRESSION,
+                  params: [{ content: `name` }],
+                  returns: createObjectMatcher({
+                    name: `[name]`,
+                    fn: {
+                      type: NodeTypes.JS_FUNCTION_EXPRESSION,
+                      returns: [
+                        {
+                          type: NodeTypes.INTERPOLATION,
+                          content: { content: `name`, isStatic: false }
+                        }
+                      ]
+                    }
+                  })
+                }
+              ]
+            }
+          ]
         }
-      }
+      ]
     })
+    expect((root as any).children[0].codegenNode.arguments[3]).toMatch(
+      PatchFlags.DYNAMIC_SLOTS + ''
+    )
+    expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
   })
 
-  test('error on named slot on component', () => {
-    const onError = jest.fn()
-    const source = `<Comp v-slot:foo>foo</Comp>`
-    parseWithSlots(source, { onError })
-    const index = source.indexOf('v-slot')
-    expect(onError.mock.calls[0][0]).toMatchObject({
-      code: ErrorCodes.X_NAMED_SLOT_ON_COMPONENT,
-      loc: {
-        source: `v-slot:foo`,
-        start: {
-          offset: index,
-          line: 1,
-          column: index + 1
-        },
-        end: {
-          offset: index + 10,
-          line: 1,
-          column: index + 11
+  describe('errors', () => {
+    test('error on extraneous children w/ named slots', () => {
+      const onError = jest.fn()
+      const source = `<Comp><template #default>foo</template>bar</Comp>`
+      parseWithSlots(source, { onError })
+      const index = source.indexOf('bar')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
+        loc: {
+          source: `bar`,
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1
+          },
+          end: {
+            offset: index + 3,
+            line: 1,
+            column: index + 4
+          }
         }
-      }
+      })
+    })
+
+    test('error on duplicated slot names', () => {
+      const onError = jest.fn()
+      const source = `<Comp><template #foo></template><template #foo></template></Comp>`
+      parseWithSlots(source, { onError })
+      const index = source.lastIndexOf('#foo')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_DUPLICATE_SLOT_NAMES,
+        loc: {
+          source: `#foo`,
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1
+          },
+          end: {
+            offset: index + 4,
+            line: 1,
+            column: index + 5
+          }
+        }
+      })
+    })
+
+    test('error on invalid mixed slot usage', () => {
+      const onError = jest.fn()
+      const source = `<Comp v-slot="foo"><template #foo></template></Comp>`
+      parseWithSlots(source, { onError })
+      const index = source.lastIndexOf('#foo')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_MIXED_SLOT_USAGE,
+        loc: {
+          source: `#foo`,
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1
+          },
+          end: {
+            offset: index + 4,
+            line: 1,
+            column: index + 5
+          }
+        }
+      })
+    })
+
+    test('error on v-slot usage on plain elements', () => {
+      const onError = jest.fn()
+      const source = `<div v-slot/>`
+      parseWithSlots(source, { onError })
+      const index = source.indexOf('v-slot')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_MISPLACED_V_SLOT,
+        loc: {
+          source: `v-slot`,
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1
+          },
+          end: {
+            offset: index + 6,
+            line: 1,
+            column: index + 7
+          }
+        }
+      })
+    })
+
+    test('error on named slot on component', () => {
+      const onError = jest.fn()
+      const source = `<Comp v-slot:foo>foo</Comp>`
+      parseWithSlots(source, { onError })
+      const index = source.indexOf('v-slot')
+      expect(onError.mock.calls[0][0]).toMatchObject({
+        code: ErrorCodes.X_NAMED_SLOT_ON_COMPONENT,
+        loc: {
+          source: `v-slot:foo`,
+          start: {
+            offset: index,
+            line: 1,
+            column: index + 1
+          },
+          end: {
+            offset: index + 10,
+            line: 1,
+            column: index + 11
+          }
+        }
+      })
     })
   })
 })
index 5dbf853ac6ce212e30ab20833a68a6e8db798006..4eff8ff834399b58e710464a6b03beda5b6713ed 100644 (file)
@@ -1,4 +1,5 @@
 import { isString } from '@vue/shared'
+import { ForParseResult } from './transforms/vFor'
 
 // Vue template is a platform-agnostic superset of HTML (syntax only).
 // More namespaces like SVG and MathML are declared by platform specific
@@ -115,6 +116,8 @@ export interface DirectiveNode extends Node {
   exp: ExpressionNode | undefined
   arg: ExpressionNode | undefined
   modifiers: string[]
+  // optional property to cache the expression parse result for v-for
+  parseResult?: ForParseResult
 }
 
 export interface SimpleExpressionNode extends Node {
@@ -249,13 +252,13 @@ export function createObjectExpression(
 }
 
 export function createObjectProperty(
-  key: Property['key'],
+  key: Property['key'] | string,
   value: Property['value']
 ): Property {
   return {
     type: NodeTypes.JS_PROPERTY,
     loc: locStub,
-    key,
+    key: isString(key) ? createSimpleExpression(key, true) : key,
     value
   }
 }
index 143deee70ec748f1aa05833161661ca47d4babfd..189e180b5fed8da0735ce5a9312d13b84f27e62d 100644 (file)
@@ -259,17 +259,23 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
   context.newline()
 }
 
+function isText(n: string | CodegenNode) {
+  return (
+    isString(n) ||
+    n.type === NodeTypes.SIMPLE_EXPRESSION ||
+    n.type === NodeTypes.TEXT ||
+    n.type === NodeTypes.INTERPOLATION ||
+    n.type === NodeTypes.COMPOUND_EXPRESSION
+  )
+}
+
 function genNodeListAsArray(
   nodes: (string | CodegenNode | TemplateChildNode[])[],
   context: CodegenContext
 ) {
   const multilines =
     nodes.length > 3 ||
-    ((!__BROWSER__ || __DEV__) &&
-      nodes.some(
-        n =>
-          isArray(n) || (!isString(n) && n.type !== NodeTypes.SIMPLE_EXPRESSION)
-      ))
+    ((!__BROWSER__ || __DEV__) && nodes.some(n => isArray(n) || !isText(n)))
   context.push(`[`)
   multilines && context.indent()
   genNodeList(nodes, context, multilines)
@@ -435,6 +441,10 @@ function genCallExpression(node: CallExpression, context: CodegenContext) {
 function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
   const { push, indent, deindent, newline, resetMapping } = context
   const { properties } = node
+  if (!properties.length) {
+    push(`{}`, node)
+    return
+  }
   const multilines =
     properties.length > 1 ||
     ((!__BROWSER__ || __DEV__) &&
index e6f51c6ca007eeeb54b31fff6c20b2df296088e3..eb23a9e02f64ca17e7edd9d4206c06496fbbc900 100644 (file)
@@ -12,7 +12,7 @@ import { transformElement } from './transforms/transformElement'
 import { transformOn } from './transforms/vOn'
 import { transformBind } from './transforms/vBind'
 import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
-import { trackSlotScopes } from './transforms/vSlot'
+import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
 import { optimizeText } from './transforms/optimizeText'
 
 export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
@@ -45,7 +45,14 @@ export function baseCompile(
     nodeTransforms: [
       transformIf,
       transformFor,
-      ...(prefixIdentifiers ? [transformExpression, trackSlotScopes] : []),
+      ...(prefixIdentifiers
+        ? [
+            // order is important
+            trackVForSlotScopes,
+            transformExpression,
+            trackSlotScopes
+          ]
+        : []),
       optimizeText,
       transformStyle,
       transformSlotOutlet,
index 6276f21b657e7851b3207e6063740087f91faa99..f67263bf191b3683ce19cac139f37c1d3771cd7c 100644 (file)
@@ -14,6 +14,7 @@ export const RESOLVE_DIRECTIVE = `resolveDirective`
 export const APPLY_DIRECTIVES = `applyDirectives`
 export const RENDER_LIST = `renderList`
 export const RENDER_SLOT = `renderSlot`
+export const CREATE_SLOTS = `createSlots`
 export const TO_STRING = `toString`
 export const MERGE_PROPS = `mergeProps`
 export const TO_HANDLERS = `toHandlers`
index d5585dd8a294f7e4100c05ec001aa13de9636fbc..e1d236253a4760113212b94a78ea327f03b08580 100644 (file)
@@ -15,8 +15,7 @@ import {
 import { isString, isArray } from '@vue/shared'
 import { CompilerError, defaultOnError } from './errors'
 import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
-import { createBlockExpression } from './utils'
-import { isVSlot } from './transforms/vSlot'
+import { isVSlot, createBlockExpression } from './utils'
 
 // There are two types of transforms:
 //
index 0f1c051dc3c84203c4a21c3d9b45f0a7fb398f4b..e4f37a22d0598128fcf423f7be203aa2e9ea5a90 100644 (file)
@@ -26,8 +26,8 @@ import {
   MERGE_PROPS,
   TO_HANDLERS
 } from '../runtimeConstants'
-import { getInnerRange } from '../utils'
-import { buildSlots, isVSlot } from './vSlot'
+import { getInnerRange, isVSlot } from '../utils'
+import { buildSlots } from './vSlot'
 
 const toValidId = (str: string): string => str.replace(/[^\w]/g, '')
 
@@ -418,7 +418,7 @@ function createDirectiveArgs(
       createObjectExpression(
         dir.modifiers.map(modifier =>
           createObjectProperty(
-            createSimpleExpression(modifier, true, loc),
+            modifier,
             createSimpleExpression(`true`, false, loc)
           )
         ),
index 0658ecc56cc7b4e04bbb2566082bd18a3270b828..36c37833997217e45cbdb287ace7ec38f0ffbd17 100644 (file)
@@ -35,11 +35,17 @@ export const transformExpression: NodeTransform = (node, context) => {
     // handle directives on element
     for (let i = 0; i < node.props.length; i++) {
       const dir = node.props[i]
-      if (dir.type === NodeTypes.DIRECTIVE) {
+      // do not process for v-for since it's special handled
+      if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
         const exp = dir.exp as SimpleExpressionNode | undefined
         const arg = dir.arg as SimpleExpressionNode | undefined
         if (exp) {
-          dir.exp = processExpression(exp, context, dir.name === 'slot')
+          dir.exp = processExpression(
+            exp,
+            context,
+            // slot args must be processed as function params
+            dir.name === 'slot'
+          )
         }
         if (arg && !arg.isStatic) {
           dir.arg = processExpression(arg, context)
index 8aec71ec6dcbb08df1019ddf28d4366cc2c0602a..fbcbdfa57408181260725b7c295a4e8bb6c3274b 100644 (file)
@@ -90,26 +90,6 @@ export const transformFor = createStructuralDirectiveTransform(
           }
 
           // finish the codegen now that all children have been traversed
-          const params: ExpressionNode[] = []
-          if (value) {
-            params.push(value)
-          }
-          if (key) {
-            if (!value) {
-              params.push(createSimpleExpression(`_`, false))
-            }
-            params.push(key)
-          }
-          if (index) {
-            if (!key) {
-              if (!value) {
-                params.push(createSimpleExpression(`_`, false))
-              }
-              params.push(createSimpleExpression(`__`, false))
-            }
-            params.push(index)
-          }
-
           let childBlock
           if (node.tagType === ElementTypes.TEMPLATE) {
             // <template v-for="...">
@@ -118,7 +98,7 @@ export const transformFor = createStructuralDirectiveTransform(
             if (keyProp) {
               childBlockProps = createObjectExpression([
                 createObjectProperty(
-                  createSimpleExpression(`key`, true),
+                  `key`,
                   keyProp.type === NodeTypes.ATTRIBUTE
                     ? createSimpleExpression(keyProp.value!.content, true)
                     : keyProp.exp!
@@ -153,7 +133,7 @@ export const transformFor = createStructuralDirectiveTransform(
 
           renderExp.arguments.push(
             createFunctionExpression(
-              params,
+              createForLoopParams(parseResult),
               childBlock,
               true /* force newline */
             )
@@ -178,21 +158,21 @@ const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
 const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
 const stripParensRE = /^\(|\)$/g
 
-interface ForParseResult {
+export interface ForParseResult {
   source: ExpressionNode
   value: ExpressionNode | undefined
   key: ExpressionNode | undefined
   index: ExpressionNode | undefined
 }
 
-function parseForExpression(
+export function parseForExpression(
   input: SimpleExpressionNode,
   context: TransformContext
-): ForParseResult | null {
+): ForParseResult | undefined {
   const loc = input.loc
   const exp = input.content
   const inMatch = exp.match(forAliasRE)
-  if (!inMatch) return null
+  if (!inMatch) return
 
   const [, LHS, RHS] = inMatch
 
@@ -274,3 +254,30 @@ function createAliasExpression(
     getInnerRange(range, offset, content.length)
   )
 }
+
+export function createForLoopParams({
+  value,
+  key,
+  index
+}: ForParseResult): ExpressionNode[] {
+  const params: ExpressionNode[] = []
+  if (value) {
+    params.push(value)
+  }
+  if (key) {
+    if (!value) {
+      params.push(createSimpleExpression(`_`, false))
+    }
+    params.push(key)
+  }
+  if (index) {
+    if (!key) {
+      if (!value) {
+        params.push(createSimpleExpression(`_`, false))
+      }
+      params.push(createSimpleExpression(`__`, false))
+    }
+    params.push(index)
+  }
+  return params
+}
index 23276d37e21288e116686808b5d313c39eccf7d1..167ca7d332b6af6a9817208d60fd880caf10748a 100644 (file)
@@ -229,8 +229,5 @@ function createChildrenCodegenNode(
 }
 
 function createKeyProperty(index: number): Property {
-  return createObjectProperty(
-    createSimpleExpression(`key`, true),
-    createSimpleExpression(index + '', false)
-  )
+  return createObjectProperty(`key`, createSimpleExpression(index + '', false))
 }
index 7075411a976e24b7cf77640f6a889c9bb5918405..29286042e772160ae141202b671f18e36af5850b 100644 (file)
@@ -15,23 +15,23 @@ import {
   createConditionalExpression,
   ConditionalExpression,
   JSChildNode,
-  SimpleExpressionNode
+  SimpleExpressionNode,
+  FunctionExpression,
+  CallExpression,
+  createCallExpression,
+  createArrayExpression
 } from '../ast'
 import { TransformContext, NodeTransform } from '../transform'
 import { createCompilerError, ErrorCodes } from '../errors'
-import { mergeExpressions, findDir } from '../utils'
-
-export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
-  p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
+import { findDir, isTemplateNode, assert, isVSlot } from '../utils'
+import { CREATE_SLOTS, RENDER_LIST } from '../runtimeConstants'
+import { parseForExpression, createForLoopParams } from './vFor'
 
 const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
   p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
 
 const defaultFallback = createSimpleExpression(`undefined`, false)
 
-const hasSameName = (slot: Property, name: string): boolean =>
-  isStaticExp(slot.key) && slot.key.content === name
-
 // A NodeTransform that tracks scope identifiers for scoped slots so that they
 // don't get prefixed by transformExpression. This transform is only applied
 // in non-browser builds with { prefixIdentifiers: true }
@@ -41,11 +41,42 @@ export const trackSlotScopes: NodeTransform = (node, context) => {
     (node.tagType === ElementTypes.COMPONENT ||
       node.tagType === ElementTypes.TEMPLATE)
   ) {
-    const vSlot = node.props.find(isVSlot)
-    if (vSlot && vSlot.exp) {
-      context.addIdentifiers(vSlot.exp)
+    const vSlot = findDir(node, 'slot')
+    if (vSlot) {
+      const { addIdentifiers, removeIdentifiers } = context
+      const slotProps = vSlot.exp
+      slotProps && addIdentifiers(slotProps)
       return () => {
-        context.removeIdentifiers(vSlot.exp!)
+        slotProps && removeIdentifiers(slotProps)
+      }
+    }
+  }
+}
+
+// A NodeTransform that tracks scope identifiers for scoped slots with v-for.
+// This transform is only applied in non-browser builds with { prefixIdentifiers: true }
+export const trackVForSlotScopes: NodeTransform = (node, context) => {
+  let vFor
+  if (
+    isTemplateNode(node) &&
+    node.props.some(isVSlot) &&
+    (vFor = findDir(node, 'for'))
+  ) {
+    const result = (vFor.parseResult = parseForExpression(
+      vFor.exp as SimpleExpressionNode,
+      context
+    ))
+    if (result) {
+      const { value, key, index } = result
+      const { addIdentifiers, removeIdentifiers } = context
+      value && addIdentifiers(value)
+      key && addIdentifiers(key)
+      index && addIdentifiers(index)
+
+      return () => {
+        value && removeIdentifiers(value)
+        key && removeIdentifiers(key)
+        index && removeIdentifiers(index)
       }
     }
   }
@@ -54,18 +85,20 @@ export const trackSlotScopes: NodeTransform = (node, context) => {
 // Instead of being a DirectiveTransform, v-slot processing is called during
 // transformElement to build the slots object for a component.
 export function buildSlots(
-  { props, children, loc }: ElementNode,
+  node: ElementNode,
   context: TransformContext
 ): {
-  slots: ObjectExpression
+  slots: ObjectExpression | CallExpression
   hasDynamicSlots: boolean
 } {
-  const slots: Property[] = []
+  const { children, loc } = node
+  const slotsProperties: Property[] = []
+  const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
   let hasDynamicSlots = false
 
   // 1. Check for default slot with slotProps on component itself.
   //    <Comp v-slot="{ prop }"/>
-  const explicitDefaultSlot = props.find(isVSlot)
+  const explicitDefaultSlot = findDir(node, 'slot', true)
   if (explicitDefaultSlot) {
     const { arg, exp, loc } = explicitDefaultSlot
     if (arg) {
@@ -73,7 +106,7 @@ export function buildSlots(
         createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc)
       )
     }
-    slots.push(buildDefaultSlot(exp, children, loc))
+    slotsProperties.push(buildDefaultSlot(exp, children, loc))
   }
 
   // 2. Iterate through children and check for template slots
@@ -86,12 +119,13 @@ export function buildSlots(
     let slotDir
 
     if (
-      slotElement.type !== NodeTypes.ELEMENT ||
-      slotElement.tagType !== ElementTypes.TEMPLATE ||
-      !(slotDir = slotElement.props.find(isVSlot))
+      !isTemplateNode(slotElement) ||
+      !(slotDir = findDir(slotElement, 'slot', true))
     ) {
       // not a <template v-slot>, skip.
-      extraneousChild = extraneousChild || slotElement
+      if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) {
+        extraneousChild = slotElement
+      }
       continue
     }
 
@@ -126,76 +160,78 @@ export function buildSlots(
       slotChildren.length ? slotChildren[0].loc : slotLoc
     )
 
-    // check if this slot is conditional (v-if/else/else-if)
+    // check if this slot is conditional (v-if/v-for)
     let vIf: DirectiveNode | undefined
     let vElse: DirectiveNode | undefined
+    let vFor: DirectiveNode | undefined
     if ((vIf = findDir(slotElement, 'if'))) {
       hasDynamicSlots = true
-      slots.push(
-        createObjectProperty(
-          slotName,
-          createConditionalExpression(vIf.exp!, slotFunction, defaultFallback)
+      dynamicSlots.push(
+        createConditionalExpression(
+          vIf.exp!,
+          buildDynamicSlot(slotName, slotFunction),
+          defaultFallback
         )
       )
     } else if (
-      (vElse = findDir(slotElement, /^else(-if)?$/, true /* allow empty */))
+      (vElse = findDir(slotElement, /^else(-if)?$/, true /* allowEmpty */))
     ) {
-      hasDynamicSlots = true
-
-      // find adjacent v-if slot
-      let baseIfSlot: Property | undefined
-      let baseIfSlotWithSameName: Property | undefined
-      let i = slots.length
-      while (i--) {
-        if (slots[i].value.type === NodeTypes.JS_CONDITIONAL_EXPRESSION) {
-          baseIfSlot = slots[i]
-          if (staticSlotName && hasSameName(baseIfSlot, staticSlotName)) {
-            baseIfSlotWithSameName = baseIfSlot
-            break
-          }
+      // find adjacent v-if
+      let j = i
+      let prev
+      while (j--) {
+        prev = children[j]
+        if (prev.type !== NodeTypes.COMMENT) {
+          break
         }
       }
-      if (!baseIfSlot) {
-        context.onError(
-          createCompilerError(ErrorCodes.X_ELSE_NO_ADJACENT_IF, vElse.loc)
-        )
-        continue
-      }
-
-      if (baseIfSlotWithSameName) {
-        // v-else branch has same slot name with base v-if branch
-        let conditional = baseIfSlotWithSameName.value as ConditionalExpression
-        // locate the deepest conditional in case we have nested ones
+      if (prev && isTemplateNode(prev) && findDir(prev, 'if')) {
+        // remove node
+        children.splice(i, 1)
+        i--
+        __DEV__ && assert(dynamicSlots.length > 0)
+        // attach this slot to previous conditional
+        let conditional = dynamicSlots[
+          dynamicSlots.length - 1
+        ] as ConditionalExpression
         while (
           conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
         ) {
           conditional = conditional.alternate
         }
-        // attach the v-else branch to the base v-if's conditional expression
         conditional.alternate = vElse.exp
           ? createConditionalExpression(
               vElse.exp,
-              slotFunction,
+              buildDynamicSlot(slotName, slotFunction),
               defaultFallback
             )
-          : slotFunction
+          : buildDynamicSlot(slotName, slotFunction)
       } else {
-        // not the same slot name. generate a separate property.
-        slots.push(
-          createObjectProperty(
-            slotName,
-            createConditionalExpression(
-              // negate base branch condition
-              mergeExpressions(
-                `!(`,
-                (baseIfSlot.value as ConditionalExpression).test,
-                `)`,
-                ...(vElse.exp ? [` && (`, vElse.exp, `)`] : [])
-              ),
-              slotFunction,
-              defaultFallback
+        context.onError(
+          createCompilerError(ErrorCodes.X_ELSE_NO_ADJACENT_IF, vElse.loc)
+        )
+      }
+    } else if ((vFor = findDir(slotElement, 'for'))) {
+      hasDynamicSlots = true
+      const parseResult =
+        vFor.parseResult ||
+        parseForExpression(vFor.exp as SimpleExpressionNode, context)
+      if (parseResult) {
+        // Render the dynamic slots as an array and add it to the createSlot()
+        // args. The runtime knows how to handle it appropriately.
+        dynamicSlots.push(
+          createCallExpression(context.helper(RENDER_LIST), [
+            parseResult.source,
+            createFunctionExpression(
+              createForLoopParams(parseResult),
+              buildDynamicSlot(slotName, slotFunction),
+              true
             )
-          )
+          ])
+        )
+      } else {
+        context.onError(
+          createCompilerError(ErrorCodes.X_FOR_MALFORMED_EXPRESSION, vFor.loc)
         )
       }
     } else {
@@ -209,7 +245,7 @@ export function buildSlots(
         }
         seenSlotNames.add(staticSlotName)
       }
-      slots.push(createObjectProperty(slotName, slotFunction))
+      slotsProperties.push(createObjectProperty(slotName, slotFunction))
     }
   }
 
@@ -224,11 +260,22 @@ export function buildSlots(
 
   if (!explicitDefaultSlot && !hasTemplateSlots) {
     // implicit default slot.
-    slots.push(buildDefaultSlot(undefined, children, loc))
+    slotsProperties.push(buildDefaultSlot(undefined, children, loc))
+  }
+
+  let slots: ObjectExpression | CallExpression = createObjectExpression(
+    slotsProperties,
+    loc
+  )
+  if (dynamicSlots.length) {
+    slots = createCallExpression(context.helper(CREATE_SLOTS), [
+      slots,
+      createArrayExpression(dynamicSlots)
+    ])
   }
 
   return {
-    slots: createObjectExpression(slots, loc),
+    slots,
     hasDynamicSlots
   }
 }
@@ -239,7 +286,7 @@ function buildDefaultSlot(
   loc: SourceLocation
 ): Property {
   return createObjectProperty(
-    createSimpleExpression(`default`, true),
+    `default`,
     createFunctionExpression(
       slotProps,
       children,
@@ -248,3 +295,13 @@ function buildDefaultSlot(
     )
   )
 }
+
+function buildDynamicSlot(
+  name: ExpressionNode,
+  fn: FunctionExpression
+): ObjectExpression {
+  return createObjectExpression([
+    createObjectProperty(`name`, name),
+    createObjectProperty(`fn`, fn)
+  ])
+}
index 2f661c7fa1c702e16f8e720478dc1206c8cf5cda..37cac6cc18c956be235e31c7c8c90094987b8c3a 100644 (file)
@@ -7,10 +7,10 @@ import {
   SequenceExpression,
   createSequenceExpression,
   createCallExpression,
-  ExpressionNode,
-  CompoundExpressionNode,
-  createCompoundExpression,
-  DirectiveNode
+  DirectiveNode,
+  ElementTypes,
+  TemplateChildNode,
+  RootNode
 } from './ast'
 import { parse } from 'acorn'
 import { walk } from 'estree-walker'
@@ -121,7 +121,7 @@ export function findDir(
     if (
       p.type === NodeTypes.DIRECTIVE &&
       (allowEmpty || p.exp) &&
-      p.name.match(name)
+      (isString(name) ? p.name === name : name.test(p.name))
     ) {
       return p
     }
@@ -160,17 +160,10 @@ export function createBlockExpression(
   ])
 }
 
-export function mergeExpressions(
-  ...args: (string | ExpressionNode)[]
-): CompoundExpressionNode {
-  const children: CompoundExpressionNode['children'] = []
-  for (let i = 0; i < args.length; i++) {
-    const exp = args[i]
-    if (isString(exp) || exp.type === NodeTypes.SIMPLE_EXPRESSION) {
-      children.push(exp)
-    } else {
-      children.push(...exp.children)
-    }
-  }
-  return createCompoundExpression(children)
-}
+export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
+  p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
+
+export const isTemplateNode = (
+  node: RootNode | TemplateChildNode
+): node is ElementNode =>
+  node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
diff --git a/packages/runtime-core/src/helpers/createSlots.ts b/packages/runtime-core/src/helpers/createSlots.ts
new file mode 100644 (file)
index 0000000..0113571
--- /dev/null
@@ -0,0 +1,26 @@
+import { Slot } from '../componentSlots'
+import { isArray } from '@vue/shared'
+
+interface CompiledSlotDescriptor {
+  name: string
+  fn: Slot
+}
+
+export function createSlots(
+  slots: Record<string, Slot>,
+  dynamicSlots: (CompiledSlotDescriptor | CompiledSlotDescriptor[])[]
+): Record<string, Slot> {
+  for (let i = 0; i < dynamicSlots.length; i++) {
+    const slot = dynamicSlots[i]
+    // array of dynamic slot generated by <template v-for="..." #[...]>
+    if (isArray(slot)) {
+      for (let j = 0; j < slot.length; j++) {
+        slots[slot[i].name] = slot[i].fn
+      }
+    } else {
+      // conditional single slot generated by <template v-if="..." #foo>
+      slots[slot.name] = slot.fn
+    }
+  }
+  return slots
+}
index d3a8374a9689e241fcbcda9b1ceef19af850f345..25204c10e12446feffafe1b19659072e1d1ff223 100644 (file)
@@ -43,6 +43,7 @@ export { renderList } from './helpers/renderList'
 export { toString } from './helpers/toString'
 export { toHandlers } from './helpers/toHandlers'
 export { renderSlot } from './helpers/renderSlot'
+export { createSlots } from './helpers/createSlots'
 export { capitalize, camelize } from '@vue/shared'
 
 // Internal, for integration with runtime compiler