]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler/v-slot): handle implicit default slot mixed with named slots
authorEvan You <yyx990803@gmail.com>
Mon, 6 Jan 2020 20:31:21 +0000 (15:31 -0500)
committerEvan You <yyx990803@gmail.com>
Mon, 6 Jan 2020 20:31:21 +0000 (15:31 -0500)
packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
packages/compiler-core/__tests__/transforms/vSlot.spec.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/transforms/vSlot.ts

index c7796e246e602fc3fc92e6f64927a9d1473def3f..e7f44eec76a50f81c53241e28cd22c21b71f83da 100644 (file)
@@ -15,20 +15,6 @@ return function render() {
 }"
 `;
 
-exports[`compiler: transform component slots explicit default slot 1`] = `
-"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
-
-return function render() {
-  const _ctx = this
-  const _component_Comp = resolveComponent(\\"Comp\\")
-  
-  return (openBlock(), createBlock(_component_Comp, null, {
-    default: ({ foo }) => [toString(foo), toString(_ctx.bar)],
-    _compiled: true
-  }))
-}"
-`;
-
 exports[`compiler: transform component slots implicit default slot 1`] = `
 "const { createVNode, resolveComponent, createBlock, openBlock } = Vue
 
@@ -146,6 +132,27 @@ return function render() {
 }"
 `;
 
+exports[`compiler: transform component slots named slots w/ implicit default slot 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, resolveComponent: _resolveComponent, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    
+    const _component_Comp = _resolveComponent(\\"Comp\\")
+    
+    return (_openBlock(), _createBlock(_component_Comp, null, {
+      one: () => [\\"foo\\"],
+      default: () => [
+        \\"bar\\",
+        _createVNode(\\"span\\")
+      ],
+      _compiled: true
+    }))
+  }
+}"
+`;
+
 exports[`compiler: transform component slots nested slots scoping 1`] = `
 "const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
 
@@ -169,3 +176,17 @@ return function render() {
   }))
 }"
 `;
+
+exports[`compiler: transform component slots on-component default slot 1`] = `
+"const { toString, resolveComponent, createVNode, createBlock, openBlock } = Vue
+
+return function render() {
+  const _ctx = this
+  const _component_Comp = resolveComponent(\\"Comp\\")
+  
+  return (openBlock(), createBlock(_component_Comp, null, {
+    default: ({ foo }) => [toString(foo), toString(_ctx.bar)],
+    _compiled: true
+  }))
+}"
+`;
index 56be19aadcf6dbfdad139d5a68d2bd6239be2590..b313f6e1fd5dcd46260dd6763de677bd6aa8eb67 100644 (file)
@@ -95,7 +95,7 @@ describe('compiler: transform component slots', () => {
     expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
   })
 
-  test('explicit default slot', () => {
+  test('on-component default slot', () => {
     const { root, slots } = parseWithSlots(
       `<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`,
       { prefixIdentifiers: true }
@@ -189,6 +189,43 @@ describe('compiler: transform component slots', () => {
     expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
   })
 
+  test('named slots w/ implicit default slot', () => {
+    const { root, slots } = parseWithSlots(
+      `<Comp>
+        <template #one>foo</template>bar<span/>
+      </Comp>`
+    )
+    expect(slots).toMatchObject(
+      createSlotMatcher({
+        one: {
+          type: NodeTypes.JS_FUNCTION_EXPRESSION,
+          params: undefined,
+          returns: [
+            {
+              type: NodeTypes.TEXT,
+              content: `foo`
+            }
+          ]
+        },
+        default: {
+          type: NodeTypes.JS_FUNCTION_EXPRESSION,
+          params: undefined,
+          returns: [
+            {
+              type: NodeTypes.TEXT,
+              content: `bar`
+            },
+            {
+              type: NodeTypes.ELEMENT,
+              tag: `span`
+            }
+          ]
+        }
+      })
+    )
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
   test('dynamically named slots', () => {
     const { root, slots } = parseWithSlots(
       `<Comp>
@@ -608,13 +645,13 @@ describe('compiler: transform component slots', () => {
   })
 
   describe('errors', () => {
-    test('error on extraneous children w/ named slots', () => {
+    test('error on extraneous children w/ named default slot', () => {
       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_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
+        code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
         loc: {
           source: `bar`,
           start: {
index 6fd71521b5322a0e1cecbc7b007442b0137f298e..c62d12b9f9918aabc8bd09c31840d5223d26d0b2 100644 (file)
@@ -76,7 +76,7 @@ export const enum ErrorCodes {
   X_V_SLOT_NAMED_SLOT_ON_COMPONENT,
   X_V_SLOT_MIXED_SLOT_USAGE,
   X_V_SLOT_DUPLICATE_SLOT_NAMES,
-  X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
+  X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
   X_V_SLOT_MISPLACED,
   X_V_MODEL_NO_EXPRESSION,
   X_V_MODEL_MALFORMED_EXPRESSION,
@@ -168,9 +168,9 @@ export const errorMessages: { [code: number]: string } = {
     `The default slot should also use <template> syntax when there are other ` +
     `named slots to avoid scope ambiguity.`,
   [ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
-  [ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN]:
-    `Extraneous children found when component has explicit slots. ` +
-    `These children will be ignored.`,
+  [ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN]:
+    `Extraneous children found when component already has explicitly named ` +
+    `default slot. These children will be ignored.`,
   [ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
   [ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
   [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
index f2ce7497c0a201b88538cf66121d342573369707..7548115078022da6c1eba2cdb9ae8f66b2040cb5 100644 (file)
@@ -117,9 +117,9 @@ export function buildSlots(
 
   // 1. Check for default slot with slotProps on component itself.
   //    <Comp v-slot="{ prop }"/>
-  const explicitDefaultSlot = findDir(node, 'slot', true)
-  if (explicitDefaultSlot) {
-    const { arg, exp, loc } = explicitDefaultSlot
+  const onComponentDefaultSlot = findDir(node, 'slot', true)
+  if (onComponentDefaultSlot) {
+    const { arg, exp, loc } = onComponentDefaultSlot
     if (arg) {
       context.onError(
         createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc)
@@ -131,8 +131,10 @@ export function buildSlots(
   // 2. Iterate through children and check for template slots
   //    <template v-slot:foo="{ prop }">
   let hasTemplateSlots = false
-  let extraneousChild: TemplateChildNode | undefined = undefined
+  let hasNamedDefaultSlot = false
+  const implicitDefaultChildren: TemplateChildNode[] = []
   const seenSlotNames = new Set<string>()
+
   for (let i = 0; i < children.length; i++) {
     const slotElement = children[i]
     let slotDir
@@ -142,13 +144,13 @@ export function buildSlots(
       !(slotDir = findDir(slotElement, 'slot', true))
     ) {
       // not a <template v-slot>, skip.
-      if (slotElement.type !== NodeTypes.COMMENT && !extraneousChild) {
-        extraneousChild = slotElement
+      if (slotElement.type !== NodeTypes.COMMENT) {
+        implicitDefaultChildren.push(slotElement)
       }
       continue
     }
 
-    if (explicitDefaultSlot) {
+    if (onComponentDefaultSlot) {
       // already has on-component default slot - this is incorrect usage.
       context.onError(
         createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
@@ -267,23 +269,33 @@ export function buildSlots(
           continue
         }
         seenSlotNames.add(staticSlotName)
+        if (staticSlotName === 'default') {
+          hasNamedDefaultSlot = true
+        }
       }
       slotsProperties.push(createObjectProperty(slotName, slotFunction))
     }
   }
 
-  if (hasTemplateSlots && extraneousChild) {
-    context.onError(
-      createCompilerError(
-        ErrorCodes.X_V_SLOT_EXTRANEOUS_NON_SLOT_CHILDREN,
-        extraneousChild.loc
-      )
-    )
-  }
-
-  if (!explicitDefaultSlot && !hasTemplateSlots) {
-    // implicit default slot.
-    slotsProperties.push(buildDefaultSlot(undefined, children, loc))
+  if (!onComponentDefaultSlot) {
+    if (!hasTemplateSlots) {
+      // implicit default slot (on component)
+      slotsProperties.push(buildDefaultSlot(undefined, children, loc))
+    } else if (implicitDefaultChildren.length) {
+      // implicit default slot (mixed with named slots)
+      if (hasNamedDefaultSlot) {
+        context.onError(
+          createCompilerError(
+            ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
+            implicitDefaultChildren[0].loc
+          )
+        )
+      } else {
+        slotsProperties.push(
+          buildDefaultSlot(undefined, implicitDefaultChildren, loc)
+        )
+      }
+    }
   }
 
   let slots: ObjectExpression | CallExpression = createObjectExpression(