Explorar el Código

feat: support scoped-slot usage with $slot

Evan You hace 7 años
padre
commit
7988a5541c

+ 8 - 1
flow/compiler.js

@@ -85,7 +85,7 @@ declare type ASTDirective = {
   end?: number;
   end?: number;
 };
 };
 
 
-declare type ASTNode = ASTElement | ASTText | ASTExpression;
+declare type ASTNode = ASTElement | ASTText | ASTExpression
 
 
 declare type ASTElement = {
 declare type ASTElement = {
   type: 1;
   type: 1;
@@ -167,6 +167,9 @@ declare type ASTElement = {
 
 
   // weex specific
   // weex specific
   appendAsTree?: boolean;
   appendAsTree?: boolean;
+
+  // 2.6 $slot check
+  has$Slot?: boolean
 };
 };
 
 
 declare type ASTExpression = {
 declare type ASTExpression = {
@@ -179,6 +182,8 @@ declare type ASTExpression = {
   ssrOptimizability?: number;
   ssrOptimizability?: number;
   start?: number;
   start?: number;
   end?: number;
   end?: number;
+  // 2.6 $slot check
+  has$Slot?: boolean
 };
 };
 
 
 declare type ASTText = {
 declare type ASTText = {
@@ -190,6 +195,8 @@ declare type ASTText = {
   ssrOptimizability?: number;
   ssrOptimizability?: number;
   start?: number;
   start?: number;
   end?: number;
   end?: number;
+  // 2.6 $slot check
+  has$Slot?: boolean
 };
 };
 
 
 // SFC-parser related declarations
 // SFC-parser related declarations

+ 1 - 1
src/compiler/codegen/index.js

@@ -27,7 +27,7 @@ export class CodegenState {
     this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
     this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
     this.directives = extend(extend({}, baseDirectives), options.directives)
     this.directives = extend(extend({}, baseDirectives), options.directives)
     const isReservedTag = options.isReservedTag || no
     const isReservedTag = options.isReservedTag || no
-    this.maybeComponent = (el: ASTElement) => el.component || !isReservedTag(el.tag)
+    this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
     this.onceId = 0
     this.onceId = 0
     this.staticRenderFns = []
     this.staticRenderFns = []
     this.pre = false
     this.pre = false

+ 2 - 1
src/compiler/optimizer.js

@@ -30,7 +30,7 @@ export function optimize (root: ?ASTElement, options: CompilerOptions) {
 
 
 function genStaticKeys (keys: string): Function {
 function genStaticKeys (keys: string): Function {
   return makeMap(
   return makeMap(
-    'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
+    'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,has$Slot' +
     (keys ? ',' + keys : '')
     (keys ? ',' + keys : '')
   )
   )
 }
 }
@@ -43,6 +43,7 @@ function markStatic (node: ASTNode) {
     // 2. static slot content fails for hot-reloading
     // 2. static slot content fails for hot-reloading
     if (
     if (
       !isPlatformReservedTag(node.tag) &&
       !isPlatformReservedTag(node.tag) &&
+      !node.component &&
       node.tag !== 'slot' &&
       node.tag !== 'slot' &&
       node.attrsMap['inline-template'] == null
       node.attrsMap['inline-template'] == null
     ) {
     ) {

+ 76 - 7
src/compiler/parser/index.js

@@ -5,7 +5,7 @@ import { parseHTML } from './html-parser'
 import { parseText } from './text-parser'
 import { parseText } from './text-parser'
 import { parseFilters } from './filter-parser'
 import { parseFilters } from './filter-parser'
 import { genAssignmentCode } from '../directives/model'
 import { genAssignmentCode } from '../directives/model'
-import { extend, cached, no, camelize, hyphenate } from 'shared/util'
+import { extend, cached, no, camelize, hyphenate, hasOwn } from 'shared/util'
 import { isIE, isEdge, isServerRendering } from 'core/util/env'
 import { isIE, isEdge, isServerRendering } from 'core/util/env'
 
 
 import {
 import {
@@ -44,6 +44,7 @@ let postTransforms
 let platformIsPreTag
 let platformIsPreTag
 let platformMustUseProp
 let platformMustUseProp
 let platformGetTagNamespace
 let platformGetTagNamespace
+let maybeComponent
 
 
 export function createASTElement (
 export function createASTElement (
   tag: string,
   tag: string,
@@ -73,6 +74,8 @@ export function parse (
   platformIsPreTag = options.isPreTag || no
   platformIsPreTag = options.isPreTag || no
   platformMustUseProp = options.mustUseProp || no
   platformMustUseProp = options.mustUseProp || no
   platformGetTagNamespace = options.getTagNamespace || no
   platformGetTagNamespace = options.getTagNamespace || no
+  const isReservedTag = options.isReservedTag || no
+  maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
 
 
   transforms = pluckModuleFunction(options.modules, 'transformNode')
   transforms = pluckModuleFunction(options.modules, 'transformNode')
   preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
   preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
@@ -98,7 +101,7 @@ export function parse (
 
 
   function closeElement (element) {
   function closeElement (element) {
     if (!inVPre && !element.processed) {
     if (!inVPre && !element.processed) {
-      element = processElement(element, options, currentParent)
+      element = processElement(element, options)
     }
     }
     // tree management
     // tree management
     if (!stack.length && element !== root) {
     if (!stack.length && element !== root) {
@@ -152,7 +155,7 @@ export function parse (
         { start: el.start }
         { start: el.start }
       )
       )
     }
     }
-    if (el.attrsMap.hasOwnProperty('v-for')) {
+    if (hasOwn(el.attrsMap, 'v-for')) {
       warnOnce(
       warnOnce(
         'Cannot use v-for on stateful component root element because ' +
         'Cannot use v-for on stateful component root element because ' +
         'it renders multiple elements.',
         'it renders multiple elements.',
@@ -376,8 +379,7 @@ function processRawAttrs (el) {
 
 
 export function processElement (
 export function processElement (
   element: ASTElement,
   element: ASTElement,
-  options: CompilerOptions,
-  parent: ASTElement | undefined
+  options: CompilerOptions
 ) {
 ) {
   processKey(element)
   processKey(element)
 
 
@@ -390,7 +392,7 @@ export function processElement (
   )
   )
 
 
   processRef(element)
   processRef(element)
-  processSlot(element, parent)
+  processSlot(element)
   processComponent(element)
   processComponent(element)
   for (let i = 0; i < transforms.length; i++) {
   for (let i = 0; i < transforms.length; i++) {
     element = transforms[i](element, options) || element
     element = transforms[i](element, options) || element
@@ -581,19 +583,86 @@ function processSlot (el) {
         )
         )
       }
       }
       el.slotScope = slotScope
       el.slotScope = slotScope
+      if (process.env.NODE_ENV !== 'production' && nodeHas$Slot(el)) {
+        warn('Unepxected mixed usage of `slot-scope` and `$slot`.', el)
+      }
+    } else {
+      // 2.6 $slot support
+      // Context: https://github.com/vuejs/vue/issues/9180
+      // Ideally, all slots should be compiled as functions (this is what we
+      // are doing in 3.x), but for 2.x e want to preserve complete backwards
+      // compatibility, and maintain the exact same compilation output for any
+      // code that does not use the new syntax.
+
+      // recursively check component children for presence of `$slot` in all
+      // expressions until running into a nested child component.
+      if (maybeComponent(el) && childrenHas$Slot(el)) {
+        processScopedSlots(el)
+      }
     }
     }
     const slotTarget = getBindingAttr(el, 'slot')
     const slotTarget = getBindingAttr(el, 'slot')
     if (slotTarget) {
     if (slotTarget) {
       el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
       el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
       // preserve slot as an attribute for native shadow DOM compat
       // preserve slot as an attribute for native shadow DOM compat
       // only for non-scoped slots.
       // only for non-scoped slots.
-      if (el.tag !== 'template' && !el.slotScope) {
+      if (el.tag !== 'template' && !el.slotScope && !nodeHas$Slot(el)) {
         addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
         addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
       }
       }
     }
     }
   }
   }
 }
 }
 
 
+function childrenHas$Slot (el): boolean {
+  return el.children ? el.children.some(nodeHas$Slot) : false
+}
+
+const $slotRE = /\$slot/
+function nodeHas$Slot (node): boolean {
+  // caching
+  if (hasOwn(node, 'has$Slot')) {
+    return (node.has$Slot: any)
+  }
+  if (node.type === 1) { // element
+    for (const key in node.attrsMap) {
+      if (dirRE.test(key) && $slotRE.test(node.attrsMap[key])) {
+        return (node.has$Slot = true)
+      }
+    }
+    return (node.has$Slot = childrenHas$Slot(node))
+  } else if (node.type === 2) { // expression
+    // TODO more robust logic for checking $slot usage
+    return (node.has$Slot = $slotRE.test(node.expression))
+  }
+  return false
+}
+
+function processScopedSlots (el) {
+  // 1. group children by slot target
+  const groups: any = {}
+  for (let i = 0; i < el.children.length; i++) {
+    const child = el.children[i]
+    const target = child.slotTarget || '"default"'
+    if (!groups[target]) {
+      groups[target] = []
+    }
+    groups[target].push(child)
+  }
+  // 2. for each slot group, check if the group contains $slot
+  for (const name in groups) {
+    const group = groups[name]
+    if (group.some(nodeHas$Slot)) {
+      // 3. if a group contains $slot, all nodes in that group gets assigned
+      // as a scoped slot to el and removed from children
+      el.plain = false
+      const slots = el.scopedSlots || (el.scopedSlots = {})
+      const slotContainer = slots[name] = createASTElement('template', [], el)
+      slotContainer.children = group
+      slotContainer.slotScope = '$slot'
+      el.children = el.children.filter(c => group.indexOf(c) === -1)
+    }
+  }
+}
+
 function processComponent (el) {
 function processComponent (el) {
   let binding
   let binding
   if ((binding = getBindingAttr(el, 'is'))) {
   if ((binding = getBindingAttr(el, 'is'))) {

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

@@ -613,4 +613,63 @@ describe('Component scoped slot', () => {
       expect(vm.$el.innerHTML).toBe('<p>hello</p>')
       expect(vm.$el.innerHTML).toBe('<p>hello</p>')
     }).then(done)
     }).then(done)
   })
   })
+
+  // 2.6 $slot usage
+  describe('$slot support', () => {
+    it('should work', () => {
+      const vm = new Vue({
+        template: `<foo><div>{{$slot.foo}}</div></foo>`,
+        components: { foo: { template: `<div><slot foo="hello"/></div>` }}
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe(`<div>hello</div>`)
+    })
+
+    it('should work for use of $slots in attributes', () => {
+      const vm = new Vue({
+        template: `<foo><div :id="$slot.foo"></div></foo>`,
+        components: { foo: { template: `<div><slot foo="hello"/></div>` }}
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe(`<div id="hello"></div>`)
+    })
+
+    it('should work for root text nodes', () => {
+      const vm = new Vue({
+        template: `<foo>{{$slot.foo}}</foo>`,
+        components: { foo: { template: `<div><slot foo="hello"/></div>` }}
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe(`hello`)
+    })
+
+    it('should work for mix of root text nodes and elements', () => {
+      const vm = new Vue({
+        template: `<foo>hi <div>{{ $slot.foo }}</div>{{$slot.foo}}</foo>`,
+        components: { foo: { template: `<div><slot foo="hello"/></div>` }}
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe(`hi <div>hello</div>hello`)
+    })
+
+    it('should work for named slots', () => {
+      const vm = new Vue({
+        template: `<foo><div slot="foo">{{ $slot.foo }}</div></foo>`,
+        components: { foo: { template: `<div><slot name="foo" foo="hello"/></div>` }}
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe(`<div>hello</div>`)
+    })
+
+    it('should work for mixed default and named slots', () => {
+      const vm = new Vue({
+        template: `<foo>{{ $slot.foo }}<div>{{ $slot.foo }}</div><div slot="foo">{{ $slot.foo }}</div></foo>`,
+        components: { foo: { template: `<div><slot foo="default"/><slot name="foo" foo="foo"/></div>` }}
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe(`default<div>default</div><div>foo</div>`)
+    })
+
+    it('should work for mixed $slot and non-$slot slots', () => {
+      const vm = new Vue({
+        template: `<foo>{{ $slot.foo }}<div slot="foo">static</div><div>{{ $slot.foo }}</div></foo>`,
+        components: { foo: { template: `<div><slot foo="default"/><slot name="foo"/></div>` }}
+      }).$mount()
+      expect(vm.$el.innerHTML).toBe(`default<div>default</div><div>static</div>`)
+    })
+  })
 })
 })