Skip to content

Commit 3f5c916

Browse files
committed
refactor slot distribution (fix vuejs#2312)
1 parent 54fcb5c commit 3f5c916

File tree

10 files changed

+123
-121
lines changed

10 files changed

+123
-121
lines changed

src/compiler/compile.js

+3-7
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {
1515
findRef,
1616
defineReactive,
1717
assertAsset,
18-
getAttr,
19-
hasBindAttr
18+
getAttr
2019
} from '../util/index'
2120

2221
// special binding prefixes
@@ -526,11 +525,8 @@ function makeChildLinkFn (linkFns) {
526525

527526
function checkElementDirectives (el, options) {
528527
var tag = el.tagName.toLowerCase()
529-
if (commonTagRE.test(tag)) return
530-
// special case: give named slot a higher priority
531-
// than unnamed slots
532-
if (tag === 'slot' && hasBindAttr(el, 'name')) {
533-
tag = '_namedSlot'
528+
if (commonTagRE.test(tag)) {
529+
return
534530
}
535531
var def = resolveAsset(options, 'elementDirectives', tag)
536532
if (def) {

src/compiler/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './compile'
22
export * from './transclude'
3+
export * from './scan-slots'

src/compiler/scan-slots.js

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { parseTemplate } from '../parsers/template'
2+
import {
3+
isTemplate,
4+
toArray,
5+
getBindAttr,
6+
warn
7+
} from '../util/index'
8+
9+
/**
10+
* Scan and determine slot content distribution.
11+
* We do this during transclusion instead at compile time so that
12+
* the distribution is decoupled from the compilation order of
13+
* the slots.
14+
*
15+
* @param {Element|DocumentFragment} template
16+
* @param {Element} content
17+
* @param {Vue} vm
18+
*/
19+
20+
export function scanSlots (template, content, vm) {
21+
if (!content) {
22+
return
23+
}
24+
var contents = vm._slotContents = {}
25+
var slots = template.querySelectorAll('slot')
26+
if (slots.length) {
27+
var hasDefault, slot, name
28+
for (var i = 0, l = slots.length; i < l; i++) {
29+
slot = slots[i]
30+
/* eslint-disable no-cond-assign */
31+
if (name = slot.getAttribute('name')) {
32+
select(slot, name)
33+
} else if (
34+
process.env.NODE_ENV !== 'production' &&
35+
(name = getBindAttr(slot, 'name'))
36+
) {
37+
warn('<slot :name="' + name + '">: slot names cannot be dynamic.')
38+
} else {
39+
// default slot
40+
hasDefault = true
41+
}
42+
/* eslint-enable no-cond-assign */
43+
}
44+
if (hasDefault) {
45+
contents['default'] = extractFragment(content.childNodes, content)
46+
}
47+
}
48+
49+
function select (slot, name) {
50+
// named slot
51+
var selector = '[slot="' + name + '"]'
52+
var nodes = content.querySelectorAll(selector)
53+
if (nodes.length) {
54+
contents[name] = extractFragment(nodes, content)
55+
}
56+
}
57+
}
58+
59+
/**
60+
* Extract qualified content nodes from a node list.
61+
*
62+
* @param {NodeList} nodes
63+
* @param {Element} parent
64+
* @return {DocumentFragment}
65+
*/
66+
67+
function extractFragment (nodes, parent) {
68+
var frag = document.createDocumentFragment()
69+
nodes = toArray(nodes)
70+
for (var i = 0, l = nodes.length; i < l; i++) {
71+
var node = nodes[i]
72+
if (node.parentNode === parent) {
73+
if (
74+
isTemplate(node) &&
75+
!node.hasAttribute('v-if') &&
76+
!node.hasAttribute('v-for')
77+
) {
78+
parent.removeChild(node)
79+
node = parseTemplate(node)
80+
}
81+
frag.appendChild(node)
82+
}
83+
}
84+
return frag
85+
}

src/compiler/transclude.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { parseTemplate } from '../parsers/template'
21
import { parseText } from '../parsers/text'
2+
import { parseTemplate } from '../parsers/template'
33
import {
44
warn,
55
isTemplate,

src/directives/element/index.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { slot, namedSlot as _namedSlot } from './slot'
1+
import slot from './slot'
22
import partial from './partial'
33

44
export default {
55
slot,
6-
_namedSlot, // same as slot but with higher priority
76
partial
87
}

src/directives/element/slot.js

+8-91
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,23 @@
11
import { SLOT } from '../priorities'
2-
3-
import {
4-
parseTemplate,
5-
cloneNode
6-
} from '../../parsers/template'
7-
82
import {
9-
extend,
103
extractContent,
114
replace,
12-
remove,
13-
isTemplate
5+
remove
146
} from '../../util/index'
157

16-
// This is the elementDirective that handles <content>
17-
// transclusions. It relies on the raw content of an
18-
// instance being stored as `$options._content` during
19-
// the transclude phase.
20-
21-
// We are exporting two versions, one for named and one
22-
// for unnamed, because the unnamed slots must be compiled
23-
// AFTER all named slots have selected their content. So
24-
// we need to give them different priorities in the compilation
25-
// process. (See #1965)
26-
27-
export const slot = {
8+
export default {
289

2910
priority: SLOT,
11+
params: ['name'],
3012

3113
bind () {
32-
var host = this.vm
33-
var raw = host.$options._content
34-
if (!raw) {
14+
// this was resolved during component transclusion
15+
var name = this.params.name || 'default'
16+
var content = this.vm._slotContents && this.vm._slotContents[name]
17+
if (!content || !content.hasChildNodes()) {
3518
this.fallback()
36-
return
37-
}
38-
var context = host._context
39-
var slotName = this.params && this.params.name
40-
if (!slotName) {
41-
// Default slot
42-
this.tryCompile(extractFragment(raw.childNodes, raw, true), context, host)
4319
} else {
44-
// Named slot
45-
var selector = '[slot="' + slotName + '"]'
46-
var nodes = raw.querySelectorAll(selector)
47-
if (nodes.length) {
48-
this.tryCompile(extractFragment(nodes, raw), context, host)
49-
} else {
50-
this.fallback()
51-
}
52-
}
53-
},
54-
55-
tryCompile (content, context, host) {
56-
if (content.hasChildNodes()) {
57-
this.compile(content, context, host)
58-
} else {
59-
this.fallback()
20+
this.compile(content.cloneNode(true), this.vm._context, this.vm)
6021
}
6122
},
6223

@@ -101,47 +62,3 @@ export const slot = {
10162
}
10263
}
10364
}
104-
105-
export const namedSlot = extend(extend({}, slot), {
106-
priority: slot.priority + 1,
107-
params: ['name']
108-
})
109-
110-
/**
111-
* Extract qualified content nodes from a node list.
112-
*
113-
* @param {NodeList} nodes
114-
* @param {Element} parent
115-
* @param {Boolean} main
116-
* @return {DocumentFragment}
117-
*/
118-
119-
function extractFragment (nodes, parent, main) {
120-
var frag = document.createDocumentFragment()
121-
for (var i = 0, l = nodes.length; i < l; i++) {
122-
var node = nodes[i]
123-
// if this is the main outlet, we want to skip all
124-
// previously selected nodes;
125-
// otherwise, we want to mark the node as selected.
126-
// clone the node so the original raw content remains
127-
// intact. this ensures proper re-compilation in cases
128-
// where the outlet is inside a conditional block
129-
if (main && !node.__v_selected) {
130-
append(node)
131-
} else if (!main && node.parentNode === parent) {
132-
node.__v_selected = true
133-
append(node)
134-
}
135-
}
136-
return frag
137-
138-
function append (node) {
139-
if (isTemplate(node) &&
140-
!node.hasAttribute('v-if') &&
141-
!node.hasAttribute('v-for')) {
142-
node = parseTemplate(node)
143-
}
144-
node = cloneNode(node)
145-
frag.appendChild(node)
146-
}
147-
}

src/instance/internal/lifecycle.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import Directive from '../../directive'
2-
import { replace, getAttr, isFragment } from '../../util/index'
3-
import { compile, compileRoot, transclude } from '../../compiler/index'
2+
3+
import {
4+
replace,
5+
getAttr,
6+
isFragment
7+
} from '../../util/index'
8+
9+
import {
10+
compile,
11+
compileRoot,
12+
transclude,
13+
scanSlots
14+
} from '../../compiler/index'
415

516
export default function (Vue) {
617
/**
@@ -57,6 +68,10 @@ export default function (Vue) {
5768
var contextOptions = this._context && this._context.$options
5869
var rootLinker = compileRoot(el, options, contextOptions)
5970

71+
// scan for slot distribution before compiling the content
72+
// so that it's decoupeld from slot/directive compilation order
73+
scanSlots(el, options._content, this)
74+
6075
// compile and link the rest
6176
var contentLinkFn
6277
var ctor = this.constructor

src/util/dom.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ export function removeClass (el, cls) {
245245
*
246246
* @param {Element} el
247247
* @param {Boolean} asFragment
248-
* @return {Element}
248+
* @return {Element|DocumentFragment}
249249
*/
250250

251251
export function extractContent (el, asFragment) {

test/unit/specs/directives/element/slot_spec.js

+3-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var nextTick = Vue.nextTick
44
describe('Slot Distribution', function () {
55
var el, vm, options
66
beforeEach(function () {
7+
spyWarns()
78
el = document.createElement('div')
89
options = {
910
el: el,
@@ -132,14 +133,14 @@ describe('Slot Distribution', function () {
132133
expect(el.lastChild.textContent).toBe('fallback c')
133134
})
134135

135-
it('should accept expressions in selectors', function () {
136+
it('should warn expressions in slot names', function () {
136137
el.innerHTML = '<p>one</p><p slot="two">two</p>'
137138
options.template = '<slot :name="theName"></slot>'
138139
options.data = {
139140
theName: 'two'
140141
}
141142
mount()
142-
expect(el.innerHTML).toBe('<p slot="two">two</p>')
143+
expect(hasWarned('slot names cannot be dynamic')).toBe(true)
143144
})
144145

145146
it('content should be dynamic and compiled in parent scope', function (done) {
@@ -196,21 +197,6 @@ describe('Slot Distribution', function () {
196197
})
197198
})
198199

199-
it('inline v-for', function () {
200-
el.innerHTML = '<p slot="1">1</p><p slot="2">2</p><p slot="3">3</p>'
201-
new Vue({
202-
el: el,
203-
template: '<div v-for="n in list"><slot :name="$index + 1"></slot></div>',
204-
data: {
205-
list: 0
206-
},
207-
beforeCompile: function () {
208-
this.list = this.$options._content.querySelectorAll('p').length
209-
}
210-
})
211-
expect(el.innerHTML).toBe('<div><p slot="1">1</p></div><div><p slot="2">2</p></div><div><p slot="3">3</p></div>')
212-
})
213-
214200
it('v-for + component + parent directive + transclusion', function (done) {
215201
var vm = new Vue({
216202
el: el,

test/unit/specs/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ scope.getWarnCount = function () {
2626
}
2727

2828
scope.hasWarned = function (msg, silent) {
29+
if (!_.warn.calls) {
30+
console.warn('make sure to call spyWarns() before tests.')
31+
}
2932
var count = _.warn.calls.count()
3033
var args
3134
while (count--) {

0 commit comments

Comments
 (0)