Sfoglia il codice sorgente

implement <slot> api

Evan You 10 anni fa
parent
commit
f865f3f2e5

+ 29 - 8
src/deprecations.js

@@ -15,31 +15,52 @@ if (process.env.NODE_ENV !== 'production') {
     },
 
     ADD: function () {
-      warn('$add() will be deprecated in 1.0.0. Use $set() instead.')
+      warn(
+        '$add() will be deprecated in 1.0.0. Use $set() instead. ' +
+        'See https://github.com/yyx990803/vue/issues/1171 for details.'
+      )
     },
 
     WAIT_FOR: function () {
-      warn('"wait-for" will be deprecated in 1.0.0. Use `activate` hook instead.')
+      warn(
+        '"wait-for" will be deprecated in 1.0.0. Use `activate` hook instead. ' +
+        'See https://github.com/yyx990803/vue/issues/1169 for details.'
+      )
     },
 
     STRICT_MODE: function () {
-      warn('Strict mode will default to `true` in 1.0.0.')
+      warn(
+        'Strict mode will default to `true` in 1.0.0. ' +
+        'See https://github.com/yyx990803/vue/issues/1170 for details.'
+      )
     },
 
-    CONTENT_SELECT: function () {
-      warn('<content select="..."> will be deprecated in in 1.0.0. in favor of <slot name="...">.')
+    CONTENT: function () {
+      warn(
+        '<content> insertion points will be deprecated in in 1.0.0. in favor of <slot>. ' +
+        'See https://github.com/yyx990803/vue/issues/1167 for details.'
+      )
     },
 
     DATA_AS_PROP: function () {
-      warn('$data will no longer be usable as a prop in 1.0.0.')
+      warn(
+        '$data will no longer be usable as a prop in 1.0.0. ' +
+        'See https://github.com/yyx990803/vue/issues/1198 for details.'
+      )
     },
 
     INHERIT: function () {
-      warn('The "inherit" option will be deprecated in 1.0.0.')
+      warn(
+        'The "inherit" option will be deprecated in 1.0.0. ' +
+        'See https://github.com/yyx990803/vue/issues/1198 for details.'
+      )
     },
 
     V_EL: function () {
-      warn('v-el will be deprecated in 1.0.0.')
+      warn(
+        'v-el will be deprecated in 1.0.0. ' +
+        'See https://github.com/yyx990803/vue/issues/1198 for details.'
+      )
     }
 
   }

+ 1 - 1
src/element-directives/index.js

@@ -1,2 +1,2 @@
-exports.content = require('./content')
+exports.slot = exports.content = require('./slot')
 exports.partial = require('./partial')

+ 24 - 8
src/element-directives/content.js → src/element-directives/slot.js

@@ -9,6 +9,12 @@ var clone = require('../parsers/template').clone
 module.exports = {
 
   bind: function () {
+
+    this.isSlot = this.el.tagName === 'SLOT'
+    if (process.env.NODE_ENV !== 'production' && !this.isSlot) {
+      _.deprecation.CONTENT()
+    }
+
     var vm = this.vm
     var host = vm
     // we need find the content context, which is the
@@ -22,8 +28,12 @@ module.exports = {
       this.fallback()
       return
     }
+
     var context = host._context
-    var selector = this._checkParam('select')
+    var selector = this.isSlot
+      ? this._checkParam('name')
+      : this._checkParam('select')
+
     if (!selector) {
       // Default content
       var self = this
@@ -44,12 +54,10 @@ module.exports = {
         compileDefaultContent()
       }
     } else {
-
-      if (process.env.NODE_ENV !== 'production') {
-        _.deprecation.CONTENT_SELECT()
-      }
-
       // select content
+      if (this.isSlot) {
+        selector = '[slot="' + selector + '"]'
+      }
       var nodes = raw.querySelectorAll(selector)
       if (nodes.length) {
         content = extractFragment(nodes, raw)
@@ -108,11 +116,19 @@ function extractFragment (nodes, parent, main) {
     // intact. this ensures proper re-compilation in cases
     // where the outlet is inside a conditional block
     if (main && !node.__v_selected) {
-      frag.appendChild(clone(node))
+      append(node)
     } else if (!main && node.parentNode === parent) {
       node.__v_selected = true
-      frag.appendChild(clone(node))
+      append(node)
     }
   }
   return frag
+
+  function append (node) {
+    node = clone(node)
+    if (node.attributes) {
+      node.removeAttribute('slot')
+    }
+    frag.appendChild(node)
+  }
 }

+ 1 - 1
src/util/options.js

@@ -306,7 +306,7 @@ function guardArrayAssets (assets) {
 exports.mergeOptions = function merge (parent, child, vm) {
 
   if (process.env.NODE_ENV !== 'production') {
-    if (child.inherit) {
+    if (child.inherit && !child._repeat) {
       _.deprecation.INHERIT()
     }
   }

+ 335 - 0
test/unit/specs/element-directives/slot_spec.js

@@ -0,0 +1,335 @@
+var Vue = require('../../../../src/vue')
+var _ = require('../../../../src/util')
+
+describe('Slot Distribution', function () {
+
+  var el, vm, options
+  beforeEach(function () {
+    el = document.createElement('div')
+    options = {
+      el: el
+    }
+  })
+
+  function mount () {
+    vm = new Vue(options)
+  }
+
+  it('no content', function () {
+    options.template = '<div><slot></slot></div>'
+    mount()
+    expect(el.firstChild.childNodes.length).toBe(0)
+  })
+
+  it('default content', function () {
+    el.innerHTML = '<p>hi</p>'
+    options.template = '<div><slot></slot></div>'
+    mount()
+    expect(el.firstChild.tagName).toBe('DIV')
+    expect(el.firstChild.firstChild.tagName).toBe('P')
+    expect(el.firstChild.firstChild.textContent).toBe('hi')
+  })
+
+  it('no template auto content', function () {
+    el.innerHTML = '<p>hi</p>'
+    options._asComponent = true
+    mount()
+    expect(el.firstChild.tagName).toBe('P')
+    expect(el.firstChild.textContent).toBe('hi')
+  })
+
+  it('fallback content', function () {
+    options.template = '<slot><p>fallback</p></slot>'
+    mount()
+    expect(el.firstChild.tagName).toBe('P')
+    expect(el.firstChild.textContent).toBe('fallback')
+  })
+
+  it('fallback content with multiple named slots', function () {
+    el.innerHTML = '<p slot="b">slot b</p>'
+    options.template =
+      '<slot name="a"><p>fallback a</p></slot>' +
+      '<slot name="b">fallback b</slot>'
+    mount()
+    expect(el.childNodes.length).toBe(2)
+    expect(el.firstChild.textContent).toBe('fallback a')
+    expect(el.lastChild.textContent).toBe('slot b')
+  })
+
+  it('selector matching multiple elements', function () {
+    el.innerHTML = '<p slot="t">1</p><div></div><p slot="t">2</p>'
+    options.template = '<slot name="t"></slot>'
+    mount()
+    expect(el.innerHTML).toBe('<p>1</p><p>2</p>')
+  })
+
+  it('default content should only render parts not selected', function () {
+    el.innerHTML = '<div>hi</div><p slot="a">1</p><p slot="b">2</p>'
+    options.template =
+      '<slot name="a"></slot>' +
+      '<slot></slot>' +
+      '<slot name="b"></slot>'
+    mount()
+    expect(el.innerHTML).toBe('<p>1</p><div>hi</div><p>2</p>')
+  })
+
+  it('content transclusion with replace', function () {
+    el.innerHTML = '<p>hi</p>'
+    options.template = '<div><div><slot></slot></div></div>'
+    options.replace = true
+    mount()
+    var res = vm.$el
+    expect(res).not.toBe(el)
+    expect(res.firstChild.tagName).toBe('DIV')
+    expect(res.firstChild.firstChild.tagName).toBe('P')
+    expect(res.firstChild.firstChild.textContent).toBe('hi')
+  })
+
+  it('block instance content transclusion', function () {
+    el.innerHTML = '<p slot="p">hi</p><span slot="span">ho</span>'
+    options.template = '<div></div><slot name="p"></slot><slot name="span"></slot>'
+    options.replace = true
+    mount()
+    expect(getChild(1).tagName).toBe('DIV')
+    expect(getChild(2).tagName).toBe('P')
+    expect(getChild(3).tagName).toBe('SPAN')
+
+    function getChild (n) {
+      var el = vm._fragmentStart
+      while (n--) {
+        el = el.nextSibling
+      }
+      return el
+    }
+  })
+
+  it('name should only match children', function () {
+    el.innerHTML =
+      '<p slot="b">select b</p>' +
+      '<span><p slot="b">nested b</p></span>' +
+      '<span><p slot="c">nested c</p></span>'
+    options.template =
+      '<slot name="a"><p>fallback a</p></slot>' +
+      '<slot name="b">fallback b</slot>' +
+      '<slot name="c">fallback c</slot>'
+    mount()
+    expect(el.childNodes.length).toBe(3)
+    expect(el.firstChild.textContent).toBe('fallback a')
+    expect(el.childNodes[1].textContent).toBe('select b')
+    expect(el.lastChild.textContent).toBe('fallback c')
+  })
+
+  it('should accept expressions in selectors', function () {
+    el.innerHTML = '<p>one</p><p slot="two">two</p>'
+    options.template = '<slot name="{{theName}}"></slot>'
+    options.data = {
+      theName: 'two'
+    }
+    mount()
+    expect(el.innerHTML).toBe('<p>two</p>')
+  })
+
+  it('content should be dynamic and compiled in parent scope', function (done) {
+    var vm = new Vue({
+      el: el,
+      data: {
+        msg: 'hello'
+      },
+      template: '<test>{{msg}}</test>',
+      components: {
+        test: {
+          template: '<slot></slot>'
+        }
+      }
+    })
+    expect(el.innerHTML).toBe('<test>hello</test>')
+    vm.msg = 'what'
+    _.nextTick(function () {
+      expect(el.innerHTML).toBe('<test>what</test>')
+      done()
+    })
+  })
+
+  it('v-if with content transclusion', function (done) {
+    var vm = new Vue({
+      el: el,
+      data: {
+        a: 1,
+        show: true
+      },
+      template: '<test show="{{show}}">{{a}}</test>',
+      components: {
+        test: {
+          props: ['show'],
+          template: '<div v-if="show"><slot></cotent></div>'
+        }
+      }
+    })
+    expect(el.textContent).toBe('1')
+    vm.a = 2
+    _.nextTick(function () {
+      expect(el.textContent).toBe('2')
+      vm.show = false
+      _.nextTick(function () {
+        expect(el.textContent).toBe('')
+        vm.show = true
+        vm.a = 3
+        _.nextTick(function () {
+          expect(el.textContent).toBe('3')
+          done()
+        })
+      })
+    })
+  })
+
+  it('inline v-repeat', function () {
+    el.innerHTML = '<p slot="1">1</p><p slot="2">2</p><p slot="3">3</p>'
+    new Vue({
+      el: el,
+      template: '<div v-repeat="list"><slot name="{{$index + 1}}"></slot></div>',
+      data: {
+        list: 0
+      },
+      beforeCompile: function () {
+        this.list = this.$options._content.querySelectorAll('p').length
+      }
+    })
+    expect(el.innerHTML).toBe('<div><p>1</p></div><div><p>2</p></div><div><p>3</p></div>')
+  })
+
+  it('v-repeat + component + parent directive + transclusion', function (done) {
+    var vm = new Vue({
+      el: el,
+      template: '<test v-repeat="list" v-class="cls">{{msg}}</test>',
+      data: {
+        cls: 'parent',
+        msg: 'hi',
+        list: [{a: 1}, {a: 2}, {a: 3}]
+      },
+      components: {
+        test: {
+          replace: true,
+          template: '<div class="child">{{a}} <slot></slot></div>'
+        }
+      }
+    })
+    var markup = vm.list.map(function (item) {
+      return '<div class="child parent">' + item.a + ' hi</div>'
+    }).join('')
+    expect(el.innerHTML).toBe(markup)
+    vm.msg = 'ho'
+    markup = vm.list.map(function (item) {
+      return '<div class="child parent">' + item.a + ' ho</div>'
+    }).join('')
+    _.nextTick(function () {
+      expect(el.innerHTML).toBe(markup)
+      done()
+    })
+  })
+
+  it('nested transclusions', function (done) {
+    vm = new Vue({
+      el: el,
+      template:
+        '<testa>' +
+          '<testb>' +
+            '<div v-repeat="list">{{$value}}</div>' +
+          '</testb>' +
+        '</testa>',
+      data: {
+        list: [1, 2]
+      },
+      components: {
+        testa: { template: '<slot></slot>' },
+        testb: { template: '<slot></slot>' }
+      }
+    })
+    expect(el.innerHTML).toBe(
+      '<testa><testb>' +
+        '<div>1</div><div>2</div>' +
+      '</testb></testa>'
+    )
+    vm.list.push(3)
+    _.nextTick(function () {
+      expect(el.innerHTML).toBe(
+        '<testa><testb>' +
+          '<div>1</div><div>2</div><div>3</div>' +
+        '</testb></testa>'
+      )
+      done()
+    })
+  })
+
+  it('nested transclusion, container dirs & props', function (done) {
+    vm = new Vue({
+      el: el,
+      template:
+        '<testa>' +
+          '<testb v-if="ok" prop="{{msg}}"></testb>' +
+        '</testa>',
+      data: {
+        ok: false,
+        msg: 'hello'
+      },
+      components: {
+        testa: { template: '<slot></slot>' },
+        testb: {
+          props: ['prop'],
+          template: '{{prop}}'
+        }
+      }
+    })
+    expect(el.innerHTML).toBe('<testa></testa>')
+    vm.ok = true
+    _.nextTick(function () {
+      expect(el.innerHTML).toBe('<testa><testb>hello</testb></testa>')
+      done()
+    })
+  })
+
+  // #1010
+  it('v-for inside transcluded content', function () {
+    vm = new Vue({
+      el: el,
+      template:
+        '<testa>' +
+          '{{inner}} {{outer}}' +
+          '<div v-for="item in list"> {{item.inner}} {{outer}}</div>' +
+        '</testa>',
+      data: {
+        outer: 'outer',
+        inner: 'parent-inner',
+        list: [
+          { inner: 'list-inner' }
+        ]
+      },
+      components: {
+        testa: {
+          data: function () {
+            return {
+              inner: 'component-inner'
+            }
+          },
+          template: '<slot></slot>'
+        }
+      }
+    })
+    expect(el.textContent).toBe('parent-inner outer list-inner outer')
+  })
+
+  it('single content outlet with replace: true', function () {
+    vm = new Vue({
+      el: el,
+      template:
+        '<test><p>1</p><p>2</p></test>',
+      components: {
+        test: {
+          template: '<slot></slot>',
+          replace: true
+        }
+      }
+    })
+    expect(el.innerHTML).toBe('<p>1</p><p>2</p>')
+  })
+
+})