Kaynağa Gözat

feat: support slot-props and its shorthand

See https://github.com/vuejs/vue/issues/9306 for more details.
Evan You 7 yıl önce
ebeveyn
işleme
584e89da4a

+ 78 - 40
src/compiler/parser/index.js

@@ -45,6 +45,7 @@ let postTransforms
 let platformIsPreTag
 let platformMustUseProp
 let platformGetTagNamespace
+let maybeComponent
 
 export function createASTElement (
   tag: string,
@@ -74,6 +75,8 @@ export function parse (
   platformIsPreTag = options.isPreTag || no
   platformMustUseProp = options.mustUseProp || no
   platformGetTagNamespace = options.getTagNamespace || no
+  const isReservedTag = options.isReservedTag || no
+  maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
 
   transforms = pluckModuleFunction(options.modules, 'transformNode')
   preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
@@ -390,7 +393,8 @@ export function processElement (
   )
 
   processRef(element)
-  processSlot(element)
+  processSlotContent(element)
+  processSlotOutlet(element)
   processComponent(element)
   for (let i = 0; i < transforms.length; i++) {
     element = transforms[i](element, options) || element
@@ -542,7 +546,79 @@ function processOnce (el) {
   }
 }
 
-function processSlot (el) {
+// handle content being passed to a component as slot,
+// e.g. <template slot="xxx">, <div slot-scope="xxx">
+function processSlotContent (el) {
+  let slotScope
+  if (el.tag === 'template') {
+    slotScope = getAndRemoveAttr(el, 'scope')
+    /* istanbul ignore if */
+    if (process.env.NODE_ENV !== 'production' && slotScope) {
+      warn(
+        `the "scope" attribute for scoped slots have been deprecated and ` +
+        `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
+        `can also be used on plain elements in addition to <template> to ` +
+        `denote scoped slots.`,
+        el.rawAttrsMap['scope'],
+        true
+      )
+    }
+    el.slotScope = (
+      slotScope ||
+      getAndRemoveAttr(el, 'slot-scope') ||
+      // new in 2.6: slot-props and its shorthand works the same as slot-scope
+      // when used on <template> containers
+      getAndRemoveAttr(el, 'slot-props') ||
+      getAndRemoveAttr(el, '()')
+    )
+  } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
+    /* istanbul ignore if */
+    if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
+      warn(
+        `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
+        `(v-for takes higher priority). Use a wrapper <template> for the ` +
+        `scoped slot to make it clearer.`,
+        el.rawAttrsMap['slot-scope'],
+        true
+      )
+    }
+    el.slotScope = slotScope
+  } else {
+    // 2.6: slot-props on component, denotes default slot
+    slotScope = getAndRemoveAttr(el, 'slot-props') || getAndRemoveAttr(el, '()')
+    if (slotScope) {
+      if (process.env.NODE_ENV !== 'production' && !maybeComponent(el)) {
+        warn(
+          `slot-props cannot be used on non-component elements.`,
+          el.rawAttrsMap['slot-props'] || el.rawAttrsMap['()']
+        )
+      }
+      // add the component's children to its default slot
+      const slots = el.scopedSlots || (el.scopedSlots = {})
+      const slotContainer = slots[`"default"`] = createASTElement('template', [], el)
+      slotContainer.children = el.children
+      slotContainer.slotScope = slotScope
+      // remove children as they are returned from scopedSlots now
+      el.children = []
+      // mark el non-plain so data gets generated
+      el.plain = false
+    }
+  }
+
+  // slot="xxx"
+  const slotTarget = getBindingAttr(el, 'slot')
+  if (slotTarget) {
+    el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
+    // preserve slot as an attribute for native shadow DOM compat
+    // only for non-scoped slots.
+    if (el.tag !== 'template' && !el.slotScope) {
+      addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
+    }
+  }
+}
+
+// handle <slot/> outlets
+function processSlotOutlet (el) {
   if (el.tag === 'slot') {
     el.slotName = getBindingAttr(el, 'name')
     if (process.env.NODE_ENV !== 'production' && el.key) {
@@ -553,44 +629,6 @@ function processSlot (el) {
         getRawBindingAttr(el, 'key')
       )
     }
-  } else {
-    let slotScope
-    if (el.tag === 'template') {
-      slotScope = getAndRemoveAttr(el, 'scope')
-      /* istanbul ignore if */
-      if (process.env.NODE_ENV !== 'production' && slotScope) {
-        warn(
-          `the "scope" attribute for scoped slots have been deprecated and ` +
-          `replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
-          `can also be used on plain elements in addition to <template> to ` +
-          `denote scoped slots.`,
-          el.rawAttrsMap['scope'],
-          true
-        )
-      }
-      el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
-    } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
-      /* istanbul ignore if */
-      if (process.env.NODE_ENV !== 'production' && el.attrsMap['v-for']) {
-        warn(
-          `Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
-          `(v-for takes higher priority). Use a wrapper <template> for the ` +
-          `scoped slot to make it clearer.`,
-          el.rawAttrsMap['slot-scope'],
-          true
-        )
-      }
-      el.slotScope = slotScope
-    }
-    const slotTarget = getBindingAttr(el, 'slot')
-    if (slotTarget) {
-      el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
-      // preserve slot as an attribute for native shadow DOM compat
-      // only for non-scoped slots.
-      if (el.tag !== 'template' && !el.slotScope) {
-        addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
-      }
-    }
   }
 }
 

+ 101 - 0
test/unit/features/component/component-scoped-slot.spec.js

@@ -631,4 +631,105 @@ describe('Component scoped slot', () => {
       expect(vm.$el.innerHTML).toBe('<p>hello</p>')
     }).then(done)
   })
+
+  // new in 2.6
+  describe('slot-props syntax', () => {
+    const Foo = {
+      render(h) {
+        return h('div', [
+          this.$scopedSlots.default && this.$scopedSlots.default('from foo default'),
+          this.$scopedSlots.one && this.$scopedSlots.one('from foo one'),
+          this.$scopedSlots.two && this.$scopedSlots.two('from foo two')
+        ])
+      }
+    }
+
+    const Bar = {
+      render(h) {
+        return this.$scopedSlots.default && this.$scopedSlots.default('from bar')[0]
+      }
+    }
+
+    const Baz = {
+      render(h) {
+        return this.$scopedSlots.default && this.$scopedSlots.default('from baz')[0]
+      }
+    }
+
+    function runSuite(syntax) {
+      it('default slot', () => {
+        const vm = new Vue({
+          template: `<foo ${syntax}="foo">{{ foo }}<div>{{ foo }}</div></foo>`,
+          components: { Foo }
+        }).$mount()
+        expect(vm.$el.innerHTML).toBe(`from foo default<div>from foo default</div>`)
+      })
+
+      it('nested default slots', () => {
+        const vm = new Vue({
+          template: `
+            <foo ${syntax}="foo">
+              <bar ${syntax}="bar">
+                <baz ${syntax}="baz">
+                  {{ foo }} | {{ bar }} | {{ baz }}
+                </baz>
+              </bar>
+            </foo>
+          `,
+          components: { Foo, Bar, Baz }
+        }).$mount()
+        expect(vm.$el.innerHTML.trim()).toBe(`from foo default | from bar | from baz`)
+      })
+
+      it('default + named slots', () => {
+        const vm = new Vue({
+          template: `
+            <foo ()="foo">
+              {{ foo }}
+              <template slot="one" ${syntax}="one">
+                {{ one }}
+              </template>
+              <template slot="two" ${syntax}="two">
+                {{ two }}
+              </template>
+            </foo>
+          `,
+          components: { Foo }
+        }).$mount()
+        expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo default from foo one from foo two`)
+      })
+
+      it('nested + named + default slots', () => {
+        const vm = new Vue({
+          template: `
+            <foo>
+              <template slot="one" ${syntax}="one">
+                <bar ${syntax}="bar">
+                  {{ one }} {{ bar }}
+                </bar>
+              </template>
+              <template slot="two" ${syntax}="two">
+                <baz ${syntax}="baz">
+                  {{ two }} {{ baz }}
+                </baz>
+              </template>
+            </foo>
+          `,
+          components: { Foo, Bar, Baz }
+        }).$mount()
+        expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`from foo one from bar from foo two from baz`)
+      })
+
+      it('should warn slot-props usage on non-component elements', () => {
+        const vm = new Vue({
+          template: `<div ${syntax}="foo"/>`
+        }).$mount()
+        expect(`slot-props cannot be used on non-component elements`).toHaveBeenWarned()
+      })
+    }
+
+    // run tests for both full syntax and shorthand
+    runSuite('slot-props')
+    runSuite('()')
+  })
 })