Skip to content

Commit ec8e9f5

Browse files
committed
feat(emuldated_shadow_dom): implement intermediate content tags
1 parent bc8e517 commit ec8e9f5

File tree

11 files changed

+549
-191
lines changed

11 files changed

+549
-191
lines changed

modules/core/src/compiler/element_injector.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,10 @@ export class ElementInjector extends TreeNode {
325325
return pb !== _undefined && isPresent(pb);
326326
}
327327

328+
forElement(el):boolean {
329+
return this._preBuiltObjects.element.domElement === el;
330+
}
331+
328332
getComponent() {
329333
if (this._proto._binding0IsComponent) {
330334
return this._obj0;

modules/core/src/compiler/shadow_dom_emulation/content_tag.js

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,98 @@ import {Decorator} from '../../annotations/annotations';
22
import {SourceLightDom, DestinationLightDom, LightDom} from './light_dom';
33
import {Inject} from 'di/di';
44
import {Element, Node, DOM} from 'facade/dom';
5+
import {isPresent} from 'facade/lang';
56
import {List, ListWrapper} from 'facade/collection';
67
import {NgElement} from 'core/dom/element';
78

89
var _scriptTemplate = DOM.createScriptTag('type', 'ng/content')
910

10-
@Decorator({
11-
selector: 'content'
12-
})
13-
export class Content {
14-
_destinationLightDom:LightDom;
15-
16-
_beginScript:Element;
17-
_endScript:Element;
18-
19-
select:string;
20-
21-
constructor(@Inject(DestinationLightDom) destinationLightDom, contentEl:NgElement) {
22-
this._destinationLightDom = destinationLightDom;
11+
class ContentStrategy {
12+
nodes;
13+
insert(nodes:List<Nodes>){}
14+
}
2315

24-
this.select = contentEl.getAttribute('select');
16+
/**
17+
* An implementation of the content tag that is used by transcluding components.
18+
* It is used when the content tag is not a direct child of another component,
19+
* and thus does not affect redistribution.
20+
*/
21+
class RenderedContent extends ContentStrategy {
22+
beginScript:Element;
23+
endScript:Element;
24+
nodes:List<Node>;
2525

26-
this._replaceContentElementWithScriptTags(contentEl.domElement);
26+
constructor(el:Element) {
27+
this._replaceContentElementWithScriptTags(el);
28+
this.nodes = [];
2729
}
2830

2931
insert(nodes:List<Node>) {
30-
DOM.insertAllBefore(this._endScript, nodes);
31-
this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this._endScript : nodes[0]);
32+
this.nodes = nodes;
33+
DOM.insertAllBefore(this.endScript, nodes);
34+
this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this.endScript : nodes[0]);
3235
}
3336

3437
_replaceContentElementWithScriptTags(contentEl:Element) {
35-
this._beginScript = DOM.clone(_scriptTemplate);
36-
this._endScript = DOM.clone(_scriptTemplate);
38+
this.beginScript = DOM.clone(_scriptTemplate);
39+
this.endScript = DOM.clone(_scriptTemplate);
3740

38-
DOM.insertBefore(contentEl, this._beginScript);
39-
DOM.insertBefore(contentEl, this._endScript);
41+
DOM.insertBefore(contentEl, this.beginScript);
42+
DOM.insertBefore(contentEl, this.endScript);
4043
DOM.removeChild(DOM.parentElement(contentEl), contentEl);
4144
}
4245

4346
_removeNodesUntil(node:Node) {
44-
var p = DOM.parentElement(this._beginScript);
45-
for (var next = DOM.nextSibling(this._beginScript);
47+
var p = DOM.parentElement(this.beginScript);
48+
for (var next = DOM.nextSibling(this.beginScript);
4649
next !== node;
47-
next = DOM.nextSibling(this._beginScript)) {
50+
next = DOM.nextSibling(this.beginScript)) {
4851
DOM.removeChild(p, next);
4952
}
5053
}
54+
}
55+
56+
/**
57+
* An implementation of the content tag that is used by transcluding components.
58+
* It is used when the content tag is a direct child of another component,
59+
* and thus does not get rendered but only affect the distribution of its parent component.
60+
*/
61+
class IntermediateContent extends ContentStrategy {
62+
destinationLightDom:LightDom;
63+
nodes:List<Node>;
64+
65+
constructor(destinationLightDom:LightDom) {
66+
this.destinationLightDom = destinationLightDom;
67+
this.nodes = [];
68+
}
69+
70+
insert(nodes:List<Node>) {
71+
this.nodes = nodes;
72+
this.destinationLightDom.redistribute();
73+
}
74+
}
75+
76+
77+
@Decorator({
78+
selector: 'content'
79+
})
80+
export class Content {
81+
_element:Element;
82+
select:string;
83+
_strategy:ContentStrategy;
84+
85+
constructor(@Inject(DestinationLightDom) destinationLightDom, contentEl:NgElement) {
86+
this.select = contentEl.getAttribute('select');
87+
this._strategy = isPresent(destinationLightDom) ?
88+
new IntermediateContent(destinationLightDom) :
89+
new RenderedContent(contentEl.domElement);
90+
}
91+
92+
nodes():List<Node> {
93+
return this._strategy.nodes;
94+
}
95+
96+
insert(nodes:List<Node>) {
97+
this._strategy.insert(nodes);
98+
}
5199
}

modules/core/src/compiler/shadow_dom_emulation/light_dom.js

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,49 @@ import {Content} from './content_tag';
1010
export class SourceLightDom {}
1111
export class DestinationLightDom {}
1212

13+
14+
class _Root {
15+
node:Node;
16+
injector:ElementInjector;
17+
18+
constructor(node, injector) {
19+
this.node = node;
20+
this.injector = injector;
21+
}
22+
}
23+
1324
// TODO: LightDom should implement SourceLightDom and DestinationLightDom
1425
// once interfaces are supported
1526
export class LightDom {
1627
lightDomView:View;
1728
shadowDomView:View;
18-
roots:List<Node>;
29+
nodes:List<Node>;
30+
roots:List<_Root>;
1931

2032
constructor(lightDomView:View, shadowDomView:View, element:Element) {
2133
this.lightDomView = lightDomView;
2234
this.shadowDomView = shadowDomView;
23-
this.roots = DOM.childNodesAsList(element);
24-
DOM.clearNodes(element);
35+
this.nodes = DOM.childNodesAsList(element);
36+
this.roots = null;
2537
}
2638

2739
redistribute() {
28-
redistributeNodes(this.contentTags(), this.expandedDomNodes());
40+
var tags = this.contentTags();
41+
if (isPresent(tags)) {
42+
redistributeNodes(tags, this.expandedDomNodes());
43+
}
2944
}
3045

3146
contentTags(): List<Content> {
3247
return this._collectAllContentTags(this.shadowDomView, []);
3348
}
3449

3550
_collectAllContentTags(item, acc:List<Content>):List<Content> {
36-
ListWrapper.forEach(item.elementInjectors, (ei) => {
51+
var eis = item.elementInjectors;
52+
for (var i = 0; i < eis.length; ++i) {
53+
var ei = eis[i];
54+
if (isBlank(ei)) continue;
55+
3756
if (ei.hasDirective(Content)) {
3857
ListWrapper.push(acc, ei.get(Content));
3958

@@ -43,23 +62,43 @@ export class LightDom {
4362
this._collectAllContentTags(c, acc);
4463
});
4564
}
46-
});
65+
}
4766
return acc;
4867
}
4968

5069
expandedDomNodes():List {
5170
var res = [];
52-
ListWrapper.forEach(this.roots, (root) => {
53-
// TODO: vsavkin calculcate this info statically when creating light dom
54-
var viewPort = this.lightDomView.getViewPortByTemplateElement(root);
55-
if (isPresent(viewPort)) {
56-
res = ListWrapper.concat(res, viewPort.nodes());
71+
72+
var roots = this._roots();
73+
for (var i = 0; i < roots.length; ++i) {
74+
75+
var root = roots[i];
76+
var ei = root.injector;
77+
78+
if (isPresent(ei) && ei.hasPreBuiltObject(ViewPort)) {
79+
var vp = root.injector.get(ViewPort);
80+
res = ListWrapper.concat(res, vp.nodes());
81+
82+
} else if (isPresent(ei) && ei.hasDirective(Content)) {
83+
var content = root.injector.get(Content);
84+
res = ListWrapper.concat(res, content.nodes());
85+
5786
} else {
58-
ListWrapper.push(res, root);
87+
ListWrapper.push(res, root.node);
5988
}
60-
});
89+
}
6190
return res;
6291
}
92+
93+
_roots() {
94+
if (isPresent(this.roots)) return this.roots;
95+
96+
var viewInj = this.lightDomView.elementInjectors;
97+
this.roots = ListWrapper.map(this.nodes, (n) =>
98+
new _Root(n, ListWrapper.find(viewInj, (inj) => inj.forElement(n))));
99+
100+
return this.roots;
101+
}
63102
}
64103

65104
function redistributeNodes(contents:List<Content>, nodes:List<Node>) {

modules/core/src/compiler/view.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,17 @@ export class View {
155155
if (isPresent(componentDirective)) {
156156
this.componentChildViews[componentChildViewIndex++].hydrate(shadowDomAppInjector,
157157
elementInjector, elementInjector.getComponent());
158+
}
159+
}
158160

161+
// this should be moved into DOM write queue
162+
for (var i = 0; i < binders.length; ++i) {
163+
var componentDirective = binders[i].componentDirective;
164+
if (isPresent(componentDirective)) {
159165
var lightDom = this.preBuiltObjects[i].lightDom;
160-
if (isPresent(lightDom)) lightDom.redistribute();
166+
if (isPresent(lightDom)) {
167+
lightDom.redistribute();
168+
}
161169
}
162170
}
163171
}
@@ -192,16 +200,6 @@ export class View {
192200
}
193201
}
194202

195-
getViewPortByTemplateElement(node):ViewPort {
196-
if (!(node instanceof Element)) return null;
197-
198-
for (var i = 0; i < this.viewPorts.length; ++i) {
199-
if (this.viewPorts[i].templateElement === node) return this.viewPorts[i];
200-
}
201-
202-
return null;
203-
}
204-
205203
_invokeMementoForRecords(records:List<Record>) {
206204
for(var i = 0; i < records.length; ++i) {
207205
this._invokeMementoFor(records[i]);

modules/core/test/compiler/integration_spec.js

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -109,50 +109,9 @@ export function main() {
109109
});
110110
});
111111
});
112-
113-
it('should emulate content tag', (done) => {
114-
var temp = `<emulated-shadow-dom-component>` +
115-
`<div>Light</div>` +
116-
`<div template="trivial-template">DOM</div>` +
117-
`</emulated-shadow-dom-component>`;
118-
119-
function createView(pv) {
120-
var view = pv.instantiate(null);
121-
view.hydrate(new Injector([]), null, {});
122-
return view;
123-
}
124-
125-
compiler.compile(MyComp, el(temp)).
126-
then(createView).
127-
then((view) => {
128-
expect(DOM.getText(view.nodes[0])).toEqual('Before LightDOM After');
129-
done();
130-
});
131-
});
132112
});
133113
}
134114

135-
@Template({
136-
selector: '[trivial-template]'
137-
})
138-
class TrivialTemplateDirective {
139-
constructor(viewPort:ViewPort) {
140-
viewPort.create();
141-
}
142-
}
143-
144-
@Component({
145-
selector: 'emulated-shadow-dom-component',
146-
template: new TemplateConfig({
147-
inline: 'Before <content></content> After',
148-
directives: []
149-
}),
150-
shadowDom: ShadowDomEmulated
151-
})
152-
class EmulatedShadowDomCmp {
153-
154-
}
155-
156115
@Decorator({
157116
selector: '[my-dir]',
158117
bind: {'elprop':'dirProp'}
@@ -166,7 +125,7 @@ class MyDir {
166125

167126
@Component({
168127
template: new TemplateConfig({
169-
directives: [MyDir, ChildComp, SomeTemplate, EmulatedShadowDomCmp, TrivialTemplateDirective]
128+
directives: [MyDir, ChildComp, SomeTemplate]
170129
})
171130
})
172131
class MyComp {
@@ -207,3 +166,4 @@ class MyService {
207166
this.greeting = 'hello';
208167
}
209168
}
169+

modules/core/test/compiler/shadow_dom/content_tag_spec.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,31 @@ var _script = `<script type="ng/content"></script>`;
1414
export function main() {
1515
describe('Content', function() {
1616
it("should insert the nodes", () => {
17-
var lightDom = new DummyLightDom();
1817
var parent = el("<div><content></content></div>");
1918
var content = DOM.firstChild(parent);
2019

21-
var c = new Content(lightDom, new NgElement(content));
20+
var c = new Content(null, new NgElement(content));
2221
c.insert([el("<a></a>"), el("<b></b>")])
2322

2423
expect(DOM.getInnerHTML(parent)).toEqual(`${_script}<a></a><b></b>${_script}`);
2524
});
2625

2726
it("should remove the nodes from the previous insertion", () => {
28-
var lightDom = new DummyLightDom();
2927
var parent = el("<div><content></content></div>");
3028
var content = DOM.firstChild(parent);
3129

32-
var c = new Content(lightDom, new NgElement(content));
30+
var c = new Content(null, new NgElement(content));
3331
c.insert([el("<a></a>")]);
3432
c.insert([el("<b></b>")]);
3533

3634
expect(DOM.getInnerHTML(parent)).toEqual(`${_script}<b></b>${_script}`);
3735
});
3836

3937
it("should insert empty list", () => {
40-
var lightDom = new DummyLightDom();
4138
var parent = el("<div><content></content></div>");
4239
var content = DOM.firstChild(parent);
4340

44-
var c = new Content(lightDom, new NgElement(content));
41+
var c = new Content(null, new NgElement(content));
4542
c.insert([el("<a></a>")]);
4643
c.insert([]);
4744

0 commit comments

Comments
 (0)