Skip to content
This repository was archived by the owner on Sep 20, 2019. It is now read-only.

Commit 01c0f63

Browse files
committed
Merge pull request #489 from webcomponents/template.cloneNode
Fixes #488: add template.cloneNode and support document.importNode(template)
2 parents 6d0f05a + 04527ce commit 01c0f63

2 files changed

Lines changed: 279 additions & 16 deletions

File tree

src/Template/Template.js

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,41 @@
99
*/
1010

1111
// minimal template polyfill
12-
if (typeof HTMLTemplateElement === 'undefined') {
13-
(function() {
12+
(function() {
13+
var needsTemplate = (typeof HTMLTemplateElement === 'undefined');
1414

15-
var TEMPLATE_TAG = 'template';
15+
// returns true if nested templates can be cloned (they cannot be on
16+
// some impl's like Safari 8)
17+
var needsCloning = (function() {
18+
if (!needsTemplate) {
19+
var frag = document.createDocumentFragment();
20+
var t = document.createElement('template');
21+
frag.appendChild(t);
22+
t.content.appendChild(document.createElement('div'));
23+
var clone = frag.cloneNode(true);
24+
return (clone.firstChild.content.childNodes.length === 0);
25+
}
26+
})();
27+
28+
var TEMPLATE_TAG = 'template';
29+
var TemplateImpl = function() {};
30+
31+
if (needsTemplate) {
1632

1733
var contentDoc = document.implementation.createHTMLDocument('template');
1834
var canDecorate = true;
1935

2036
/**
2137
Provides a minimal shim for the <template> element.
2238
*/
23-
HTMLTemplateElement = function() {};
24-
HTMLTemplateElement.prototype = Object.create(HTMLElement.prototype);
39+
40+
TemplateImpl.prototype = Object.create(HTMLElement.prototype);
2541

2642
/**
2743
The `decorate` method moves element children to the template's `content`.
2844
NOTE: there is no support for dynamically adding elements to templates.
2945
*/
30-
HTMLTemplateElement.decorate = function(template) {
46+
TemplateImpl.decorate = function(template) {
3147
// if the template is decorated, return fast
3248
if (template.content) {
3349
return;
@@ -51,7 +67,7 @@ if (typeof HTMLTemplateElement === 'undefined') {
5167
},
5268
set: function(text) {
5369
contentDoc.body.innerHTML = text;
54-
HTMLTemplateElement.bootstrap(contentDoc);
70+
TemplateImpl.bootstrap(contentDoc);
5571
while (this.content.firstChild) {
5672
this.content.removeChild(this.content.firstChild);
5773
}
@@ -61,29 +77,33 @@ if (typeof HTMLTemplateElement === 'undefined') {
6177
},
6278
configurable: true
6379
});
80+
81+
template.cloneNode = function(deep) {
82+
return TemplateImpl.cloneNode(this, deep);
83+
};
84+
6485
} catch (err) {
6586
canDecorate = false;
6687
}
6788
}
68-
6989
// bootstrap recursively
70-
HTMLTemplateElement.bootstrap(template.content);
90+
TemplateImpl.bootstrap(template.content);
7191
};
7292

7393
/**
7494
The `bootstrap` method is called automatically and "fixes" all
7595
<template> elements in the document referenced by the `doc` argument.
7696
*/
77-
HTMLTemplateElement.bootstrap = function(doc) {
97+
TemplateImpl.bootstrap = function(doc) {
7898
var templates = doc.querySelectorAll(TEMPLATE_TAG);
7999
for (var i=0, l=templates.length, t; (i<l) && (t=templates[i]); i++) {
80-
HTMLTemplateElement.decorate(t);
100+
TemplateImpl.decorate(t);
81101
}
82102
};
83103

84104
// auto-bootstrapping for main document
85105
document.addEventListener('DOMContentLoaded', function() {
86-
HTMLTemplateElement.bootstrap(document);
106+
TemplateImpl.bootstrap(document);
87107
});
88108

89109
// Patch document.createElement to ensure newly created templates have content
@@ -92,7 +112,7 @@ if (typeof HTMLTemplateElement === 'undefined') {
92112
'use strict';
93113
var el = createElement.apply(document, arguments);
94114
if (el.localName == 'template') {
95-
HTMLTemplateElement.decorate(el);
115+
TemplateImpl.decorate(el);
96116
}
97117
return el;
98118
};
@@ -115,6 +135,91 @@ if (typeof HTMLTemplateElement === 'undefined') {
115135
function escapeData(s) {
116136
return s.replace(escapeDataRegExp, escapeReplace);
117137
}
138+
}
118139

119-
})();
120-
}
140+
// make cloning/importing work!
141+
if (needsTemplate || needsCloning) {
142+
// NOTE: we rely on this cloneNode not causing element upgrade.
143+
// This means this polyfill must load before the CE polyfill and
144+
// this would need to be re-worked if a browser supports native CE
145+
// but not <template>.
146+
var nativeCloneNode = Node.prototype.cloneNode;
147+
148+
TemplateImpl.cloneNode = function(template, deep) {
149+
var clone = nativeCloneNode.call(template);
150+
// NOTE: decorate doesn't auto-fix children because they are already
151+
// decorated so they need special clone fixup.
152+
if (this.decorate) {
153+
this.decorate(clone);
154+
}
155+
if (deep) {
156+
// NOTE: use native clone node to make sure CE's wrapped
157+
// cloneNode does not cause elements to upgrade.
158+
clone.content.appendChild(
159+
nativeCloneNode.call(template.content, true));
160+
// now ensure nested templates are cloned correctly.
161+
this.fixClonedDom(clone.content, template.content);
162+
}
163+
return clone;
164+
};
165+
166+
// Given a source and cloned subtree, find <template>'s in the cloned
167+
// subtree and replace them with cloned <template>'s from source.
168+
// We must do this because only the source templates have proper .content.
169+
TemplateImpl.fixClonedDom = function(clone, source) {
170+
// these two lists should be coincident
171+
var s$ = source.querySelectorAll(TEMPLATE_TAG);
172+
var t$ = clone.querySelectorAll(TEMPLATE_TAG);
173+
for (var i=0, l=t$.length, t, s; i<l; i++) {
174+
s = s$[i];
175+
t = t$[i];
176+
if (this.decorate) {
177+
this.decorate(s);
178+
}
179+
t.parentNode.replaceChild(s.cloneNode(true), t);
180+
}
181+
};
182+
183+
var originalImportNode = document.importNode;
184+
185+
// override all cloning to fix the cloned subtree to contain properly
186+
// cloned templates.
187+
Node.prototype.cloneNode = function(deep) {
188+
var dom = nativeCloneNode.call(this, deep);
189+
// template.content is cloned iff `deep`.
190+
if (deep) {
191+
TemplateImpl.fixClonedDom(dom, this);
192+
}
193+
return dom;
194+
};
195+
196+
// NOTE: we are cloning instead of importing <template>'s.
197+
// However, the ownerDocument of the cloned template will be correct!
198+
// This is because the native import node creates the right document owned
199+
// subtree and `fixClonedDom` inserts cloned templates into this subtree,
200+
// thus updating the owner doc.
201+
document.importNode = function(element, deep) {
202+
if (element.localName === TEMPLATE_TAG) {
203+
return TemplateImpl.cloneNode(element, deep);
204+
} else {
205+
var dom = originalImportNode.call(document, element, deep);
206+
if (deep) {
207+
TemplateImpl.fixClonedDom(dom, element);
208+
}
209+
return dom;
210+
}
211+
};
212+
213+
if (needsCloning) {
214+
HTMLTemplateElement.prototype.cloneNode = function(deep) {
215+
return TemplateImpl.cloneNode(this, deep);
216+
};
217+
}
218+
219+
}
220+
221+
if (needsTemplate) {
222+
HTMLTemplateElement = TemplateImpl;
223+
}
224+
225+
})();

tests/Template/tests.html

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,166 @@
6161
div.innerHTML = s;
6262
// innerHTML is properly escaped
6363
assert.equal(imp.innerHTML, escaped);
64-
assert.equal(imp.content.childNodes.length, div.childNodes.length);
64+
assert.equal(imp.content.textContent, div.textContent);
6565
});
66+
67+
test('clone', function() {
68+
var imp = document.createElement('template');
69+
var s = '<div>Hi</div>';
70+
imp.innerHTML = s;
71+
var clone = imp.cloneNode();
72+
assert.notEqual(clone, imp, 'element is not cloned');
73+
assert.isDefined(clone.content, 'cloned template content dne');
74+
assert.equal(clone.content.childNodes.length, 0,
75+
'non-deep cloned template.content is not empty');
76+
var deepClone = imp.cloneNode(true);
77+
assert.equal(deepClone.content.childNodes.length, 1,
78+
'deep cloned template.content is empty');
79+
assert.notEqual(imp.content.firstChild, deepClone.content.firstChild,
80+
'cloned content is not different from source');
81+
});
82+
83+
test('clone nested', function() {
84+
var imp = document.createElement('template');
85+
var s = 'a<template id="a">b<template id="b">c<template id="c">d</template></template></template>';
86+
imp.innerHTML = s;
87+
var clone = imp.cloneNode();
88+
assert.notEqual(clone, imp, 'element is not cloned');
89+
assert.isDefined(clone.content, 'cloned template content dne');
90+
assert.equal(clone.content.childNodes.length, 0,
91+
'non-deep cloned template.content is not empty');
92+
var deepClone = imp.cloneNode(true);
93+
assert.equal(deepClone.content.childNodes.length, 2,
94+
'deep cloned template.content is empty');
95+
assert.notEqual(imp.content.firstChild, deepClone.content.firstChild,
96+
'cloned content is not different from source');
97+
var nested = deepClone.content.lastChild;
98+
assert.isDefined(nested.content, 'nested cloned template content dne');
99+
assert.equal(nested.content.childNodes.length, 2,
100+
'deep cloned template.content is empty');
101+
nested = nested.content.lastChild;
102+
assert.isDefined(nested, 'nested cloned template content dne');
103+
assert.equal(nested.content.childNodes.length, 2,
104+
'deep cloned template.content is empty');
105+
nested = nested.content.lastChild;
106+
assert.isDefined(nested, 'nested cloned template content dne');
107+
assert.equal(nested.content.childNodes.length, 1,
108+
'deep cloned template.content is empty');
109+
});
110+
111+
test('clone node containing templates', function() {
112+
var imp = document.createElement('div');
113+
var t = document.createElement('template');
114+
var s = 'a<template id="a">b<template id="b">c<template id="c">d</template></template></template>';
115+
t.innerHTML = s;
116+
imp.appendChild(t);
117+
var impClone = imp.cloneNode(true);
118+
var imp = imp.firstChild;
119+
var deepClone = impClone.firstChild;
120+
assert.equal(deepClone.content.childNodes.length, 2,
121+
'deep cloned template.content is empty');
122+
assert.notEqual(imp.content.firstChild, deepClone.content.firstChild,
123+
'cloned content is not different from source');
124+
var nested = deepClone.content.lastChild;
125+
assert.isDefined(nested.content, 'nested cloned template content dne');
126+
assert.equal(nested.content.childNodes.length, 2,
127+
'deep cloned template.content is empty');
128+
nested = nested.content.lastChild;
129+
assert.isDefined(nested, 'nested cloned template content dne');
130+
assert.equal(nested.content.childNodes.length, 2,
131+
'deep cloned template.content is empty');
132+
nested = nested.content.lastChild;
133+
assert.isDefined(nested, 'nested cloned template content dne');
134+
assert.equal(nested.content.childNodes.length, 1,
135+
'deep cloned template.content is empty');
136+
});
137+
138+
test('importNode', function() {
139+
var imp = document.createElement('template');
140+
var s = '<div>Hi</div>';
141+
imp.innerHTML = s;
142+
var clone = document.importNode(imp, false);
143+
assert.notEqual(clone, imp, 'element is not cloned');
144+
assert.isDefined(clone.content, 'cloned template content dne');
145+
assert.equal(clone.content.childNodes.length, 0,
146+
'non-deep cloned template.content is not empty');
147+
var deepClone = document.importNode(imp, true);
148+
assert.equal(deepClone.content.childNodes.length, 1,
149+
'deep cloned template.content is empty');
150+
assert.notEqual(imp.content.firstChild, deepClone.content.firstChild,
151+
'cloned content is not different from source');
152+
});
153+
154+
test('importNode: nested', function() {
155+
var imp = document.createElement('template');
156+
var s = 'a<template id="a">b<template id="b">c<template id="c">d</template></template></template>';
157+
imp.innerHTML = s;
158+
var clone = document.importNode(imp, false);
159+
assert.notEqual(clone, imp, 'element is not cloned');
160+
assert.isDefined(clone.content, 'cloned template content dne');
161+
assert.equal(clone.content.childNodes.length, 0,
162+
'non-deep cloned template.content is not empty');
163+
var deepClone = document.importNode(imp, true);
164+
assert.equal(deepClone.content.childNodes.length, 2,
165+
'deep cloned template.content is empty');
166+
assert.notEqual(imp.content.firstChild, deepClone.content.firstChild,
167+
'cloned content is not different from source');
168+
var nested = deepClone.content.lastChild;
169+
assert.isDefined(nested.content, 'nested cloned template content dne');
170+
assert.equal(nested.content.childNodes.length, 2,
171+
'deep cloned template.content is empty');
172+
nested = nested.content.lastChild;
173+
assert.isDefined(nested, 'nested cloned template content dne');
174+
assert.equal(nested.content.childNodes.length, 2,
175+
'deep cloned template.content is empty');
176+
nested = nested.content.lastChild;
177+
assert.isDefined(nested, 'nested cloned template content dne');
178+
assert.equal(nested.content.childNodes.length, 1,
179+
'deep cloned template.content is empty');
180+
});
181+
182+
test('importNode: element containing nested templates', function() {
183+
var imp = document.createElement('div');
184+
var t = document.createElement('template');
185+
var s = 'a<template id="a">b<template id="b">c<template id="c">d</template></template></template>';
186+
t.innerHTML = s;
187+
imp.appendChild(t);
188+
var impClone = document.importNode(imp, true);
189+
imp = imp.firstChild;
190+
var deepClone = impClone.firstChild;
191+
assert.equal(deepClone.content.childNodes.length, 2,
192+
'deep cloned template.content is empty');
193+
assert.notEqual(imp.content.firstChild, deepClone.content.firstChild,
194+
'cloned content is not different from source');
195+
var nested = deepClone.content.lastChild;
196+
assert.isDefined(nested.content, 'nested cloned template content dne');
197+
assert.equal(nested.content.childNodes.length, 2,
198+
'deep cloned template.content is empty');
199+
nested = nested.content.lastChild;
200+
assert.isDefined(nested, 'nested cloned template content dne');
201+
assert.equal(nested.content.childNodes.length, 2,
202+
'deep cloned template.content is empty');
203+
nested = nested.content.lastChild;
204+
assert.isDefined(nested, 'nested cloned template content dne');
205+
assert.equal(nested.content.childNodes.length, 1,
206+
'deep cloned template.content is empty');
207+
});
208+
209+
test('importNode: templates have proper owner document', function() {
210+
var doc = document.implementation.createHTMLDocument('');
211+
var imp = doc.createElement('div');
212+
var t = doc.createElement('template');
213+
var s = 'a<template id="a">b<template id="b">c<template id="c">d</template></template></template>';
214+
t.innerHTML = s;
215+
imp.appendChild(t);
216+
var impClone = document.importNode(imp, true);
217+
imp = imp.firstChild;
218+
var deepClone = impClone.firstChild;
219+
assert.notEqual(imp.ownerDocument, document);
220+
assert.equal(impClone.ownerDocument, document);
221+
assert.equal(deepClone.ownerDocument, document);
222+
});
223+
66224
});
67225
</script>
68226
</body>

0 commit comments

Comments
 (0)