Skip to content

Commit 400f8bf

Browse files
committed
refactor content transclusion
1 parent 5837d2f commit 400f8bf

File tree

13 files changed

+315
-345
lines changed

13 files changed

+315
-345
lines changed

component.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"src/batcher.js",
2121
"src/cache.js",
2222
"src/compiler/compile.js",
23+
"src/compiler/content.js",
2324
"src/compiler/transclude.js",
2425
"src/config.js",
2526
"src/directive.js",

src/api/lifecycle.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ exports.$destroy = function (remove, deferCleanup) {
6464
* decompile function.
6565
*
6666
* @param {Element|DocumentFragment} el
67+
* @param {Vue} [host]
6768
* @return {Function}
6869
*/
6970

70-
exports.$compile = function (el) {
71-
return compile(el, this.$options, true)(this, el)
71+
exports.$compile = function (el, host) {
72+
return compile(el, this.$options, true, host)(this, el)
7273
}

src/compiler/compile.js

Lines changed: 13 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ module.exports = compile
2626
* @param {Element|DocumentFragment} el
2727
* @param {Object} options
2828
* @param {Boolean} partial
29-
* @param {Boolean} transcluded
29+
* @param {Vue} [host] - host vm of transcluded content
3030
* @return {Function}
3131
*/
3232

33-
function compile (el, options, partial, transcluded) {
33+
function compile (el, options, partial, host) {
3434
// link function for the node itself.
3535
var nodeLinkFn = !partial
3636
? compileRoot(el, options)
@@ -53,27 +53,17 @@ function compile (el, options, partial, transcluded) {
5353
* @return {Function|undefined}
5454
*/
5555

56-
function compositeLinkFn (vm, el) {
56+
return function compositeLinkFn (vm, el) {
5757
// save original directive count before linking
5858
// so we can capture the directives created during a
5959
// partial compilation.
6060
var originalDirCount = vm._directives.length
61-
var parentOriginalDirCount =
62-
vm.$parent && vm.$parent._directives.length
6361
// cache childNodes before linking parent, fix #657
6462
var childNodes = _.toArray(el.childNodes)
65-
// if this is a transcluded compile, linkers need to be
66-
// called in source scope, and the host needs to be
67-
// passed down.
68-
var source = transcluded ? vm.$parent : vm
69-
var host = transcluded ? vm : undefined
7063
// link
71-
if (nodeLinkFn) nodeLinkFn(source, el, host)
72-
if (childLinkFn) childLinkFn(source, childNodes, host)
73-
74-
var selfDirs = vm._directives.slice(originalDirCount)
75-
var parentDirs = vm.$parent &&
76-
vm.$parent._directives.slice(parentOriginalDirCount)
64+
if (nodeLinkFn) nodeLinkFn(vm, el, host)
65+
if (childLinkFn) childLinkFn(vm, childNodes, host)
66+
var dirs = vm._directives.slice(originalDirCount)
7767

7868
/**
7969
* The linker function returns an unlink function that
@@ -83,38 +73,15 @@ function compile (el, options, partial, transcluded) {
8373
* @param {Boolean} destroying
8474
*/
8575
return function unlink (destroying) {
86-
teardownDirs(vm, selfDirs, destroying)
87-
if (parentDirs) {
88-
teardownDirs(vm.$parent, parentDirs)
76+
var i = dirs.length
77+
while (i--) {
78+
dirs[i]._teardown()
79+
if (!destroying) {
80+
vm._directives.$remove(dirs[i])
81+
}
8982
}
9083
}
9184
}
92-
93-
// transcluded linkFns are terminal, because it takes
94-
// over the entire sub-tree.
95-
if (transcluded) {
96-
compositeLinkFn.terminal = true
97-
}
98-
99-
return compositeLinkFn
100-
}
101-
102-
/**
103-
* Teardown a subset of directives on a vm.
104-
*
105-
* @param {Vue} vm
106-
* @param {Array} dirs
107-
* @param {Boolean} destroying
108-
*/
109-
110-
function teardownDirs (vm, dirs, destroying) {
111-
var i = dirs.length
112-
while (i--) {
113-
dirs[i]._teardown()
114-
if (!destroying) {
115-
vm._directives.$remove(dirs[i])
116-
}
117-
}
11885
}
11986

12087
/**
@@ -201,13 +168,6 @@ function compileNode (node, options) {
201168

202169
function compileElement (el, options) {
203170
var hasAttrs = el.hasAttributes()
204-
if (hasAttrs && checkTransclusion(el)) {
205-
// unwrap textNode
206-
if (el.hasAttribute('__vue__wrap')) {
207-
el = el.firstChild
208-
}
209-
return compile(el, options._parent.$options, true, true)
210-
}
211171
// check element directives
212172
var linkFn = checkElementDirectives(el, options)
213173
// check terminal direcitves (repeat & if)
@@ -708,19 +668,4 @@ function directiveComparator (a, b) {
708668
a = a.def.priority || 0
709669
b = b.def.priority || 0
710670
return a > b ? 1 : -1
711-
}
712-
713-
/**
714-
* Check whether an element is transcluded
715-
*
716-
* @param {Element} el
717-
* @return {Boolean}
718-
*/
719-
720-
var transcludedFlagAttr = '__vue__transcluded'
721-
function checkTransclusion (el) {
722-
if (el.nodeType === 1 && el.hasAttribute(transcludedFlagAttr)) {
723-
el.removeAttribute(transcludedFlagAttr)
724-
return true
725-
}
726-
}
671+
}

src/compiler/content.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
var _ = require('../util')
2+
3+
// This is the elementDirective that handles <content>
4+
// transclusions. It relies on the raw content of an
5+
// instance being stored as `$options._content` during
6+
// the transclude phase.
7+
8+
module.exports = {
9+
10+
bind: function () {
11+
var vm = this.vm
12+
var contentOwner = vm
13+
// we need find the content owner, which is the closest
14+
// non-inline-repeater instance.
15+
while (contentOwner.$options._repeat) {
16+
contentOwner = contentOwner.$parent
17+
}
18+
var raw = contentOwner.$options._content
19+
var content
20+
if (!raw) {
21+
// fallback content
22+
// extract as a fragment
23+
content = _.extractContent(this.el, true)
24+
this.compile(content, vm)
25+
return
26+
}
27+
var parent = contentOwner.$parent
28+
var selector = this.el.getAttribute('select')
29+
if (!selector) {
30+
// default content
31+
// Importent: clone the rawContent before extracting
32+
// content because the <content> may be inside a v-if
33+
// and need to be compiled more than once.
34+
content = _.extractContent(raw.cloneNode(true), true)
35+
this.compile(content, parent, vm)
36+
} else {
37+
// select content
38+
selector = vm.$interpolate(selector)
39+
content = raw.querySelector(selector)
40+
// only allow top-level select
41+
if (content && content.parentNode === raw) {
42+
// same deal, clone the node for v-if
43+
content = content.cloneNode(true)
44+
this.compile(content, parent, vm)
45+
}
46+
}
47+
},
48+
49+
compile: function (content, owner, host) {
50+
if (owner) {
51+
this.unlink = owner.$compile(content, host)
52+
}
53+
_.replace(this.el, content)
54+
},
55+
56+
unbind: function () {
57+
if (this.unlink) {
58+
this.unlink()
59+
}
60+
}
61+
62+
}

src/compiler/transclude.js

Lines changed: 2 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
var _ = require('../util')
22
var config = require('../config')
33
var templateParser = require('../parsers/template')
4-
var transcludedFlagAttr = '__vue__transcluded'
54

65
/**
76
* Process an element or a DocumentFragment based on a
@@ -24,26 +23,6 @@ module.exports = function transclude (el, options) {
2423
if (options) {
2524
options._containerAttrs = extractAttrs(el)
2625
}
27-
// Mark content nodes and attrs so that the compiler
28-
// knows they should be compiled in parent scope.
29-
if (options && options._asComponent) {
30-
var i = el.childNodes.length
31-
while (i--) {
32-
var node = el.childNodes[i]
33-
if (node.nodeType === 1) {
34-
node.setAttribute(transcludedFlagAttr, '')
35-
} else if (node.nodeType === 3 && node.data.trim()) {
36-
// wrap transcluded textNodes in spans, because
37-
// raw textNodes can't be persisted through clones
38-
// by attaching attributes.
39-
var wrapper = document.createElement('span')
40-
wrapper.textContent = node.data
41-
wrapper.setAttribute('__vue__wrap', '')
42-
wrapper.setAttribute(transcludedFlagAttr, '')
43-
el.replaceChild(wrapper, node)
44-
}
45-
}
46-
}
4726
// for template tags, what we want is its content as
4827
// a documentFragment (for block instances)
4928
if (el.tagName === 'TEMPLATE') {
@@ -77,7 +56,7 @@ function transcludeTemplate (el, options) {
7756
if (!frag) {
7857
_.warn('Invalid template option: ' + template)
7958
} else {
80-
var rawContent = options._content || _.extractContent(el)
59+
options._content = _.extractContent(el)
8160
var replacer = frag.firstChild
8261
if (options.replace) {
8362
if (
@@ -88,117 +67,22 @@ function transcludeTemplate (el, options) {
8867
// block instance. (#835)
8968
replacer.hasAttribute(config.prefix + 'repeat')
9069
) {
91-
transcludeContent(frag, rawContent)
9270
return frag
9371
} else {
9472
options._replacerAttrs = extractAttrs(replacer)
9573
mergeAttrs(el, replacer)
96-
transcludeContent(replacer, rawContent)
9774
return replacer
9875
}
9976
} else {
10077
el.appendChild(frag)
101-
transcludeContent(el, rawContent)
10278
return el
10379
}
10480
}
10581
}
10682

107-
/**
108-
* Resolve <content> insertion points mimicking the behavior
109-
* of the Shadow DOM spec:
110-
*
111-
* http://w3c.github.io/webcomponents/spec/shadow/#insertion-points
112-
*
113-
* @param {Element|DocumentFragment} el
114-
* @param {Element} raw
115-
*/
116-
117-
function transcludeContent (el, raw) {
118-
var outlets = getOutlets(el)
119-
var i = outlets.length
120-
if (!i) return
121-
var outlet, select, selected, j, main
122-
123-
function isDirectChild (node) {
124-
return node.parentNode === raw
125-
}
126-
127-
// first pass, collect corresponding content
128-
// for each outlet.
129-
while (i--) {
130-
outlet = outlets[i]
131-
if (raw) {
132-
select = outlet.getAttribute('select')
133-
if (select) { // select content
134-
selected = raw.querySelectorAll(select)
135-
if (selected.length) {
136-
// according to Shadow DOM spec, `select` can
137-
// only select direct children of the host node.
138-
// enforcing this also fixes #786.
139-
selected = [].filter.call(selected, isDirectChild)
140-
}
141-
outlet.content = selected.length
142-
? selected
143-
: _.toArray(outlet.childNodes)
144-
} else { // default content
145-
main = outlet
146-
}
147-
} else { // fallback content
148-
outlet.content = _.toArray(outlet.childNodes)
149-
}
150-
}
151-
// second pass, actually insert the contents
152-
for (i = 0, j = outlets.length; i < j; i++) {
153-
outlet = outlets[i]
154-
if (outlet !== main) {
155-
insertContentAt(outlet, outlet.content)
156-
}
157-
}
158-
// finally insert the main content
159-
if (main) {
160-
insertContentAt(main, _.toArray(raw.childNodes))
161-
}
162-
}
163-
164-
/**
165-
* Get <content> outlets from the element/list
166-
*
167-
* @param {Element|Array} el
168-
* @return {Array}
169-
*/
170-
171-
var concat = [].concat
172-
function getOutlets (el) {
173-
return _.isArray(el)
174-
? concat.apply([], el.map(getOutlets))
175-
: el.querySelectorAll
176-
? _.toArray(el.querySelectorAll('content'))
177-
: []
178-
}
179-
180-
/**
181-
* Insert an array of nodes at outlet,
182-
* then remove the outlet.
183-
*
184-
* @param {Element} outlet
185-
* @param {Array} contents
186-
*/
187-
188-
function insertContentAt (outlet, contents) {
189-
// not using util DOM methods here because
190-
// parentNode can be cached
191-
var parent = outlet.parentNode
192-
for (var i = 0, j = contents.length; i < j; i++) {
193-
parent.insertBefore(contents[i], outlet)
194-
}
195-
parent.removeChild(outlet)
196-
}
197-
19883
/**
19984
* Helper to extract a component container's attribute names
200-
* into a map. The resulting map will be used in compiler to
201-
* determine whether an attribute is transcluded.
85+
* into a map.
20286
*
20387
* @param {Element} el
20488
* @return {Object}

src/directives/repeat.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ module.exports = {
132132
merged._asComponent = true
133133
merged._parent = this.vm
134134
this.template = transclude(this.template, merged)
135+
this.content = merged._content
135136
// Important: mark the template as a root node so that
136137
// custom element components don't get compiled twice.
137138
// fixes #822
@@ -365,6 +366,8 @@ module.exports = {
365366
_host: this._host,
366367
_linkFn: this._linkFn,
367368
_meta: meta,
369+
_content: this.content,
370+
_repeat: this.inherit,
368371
data: data,
369372
inherit: this.inherit,
370373
template: this.inlineTempalte

0 commit comments

Comments
 (0)