Skip to content

Commit c6f14dd

Browse files
committed
feat(viewPort): adds initial implementation of ViewPort.
ViewPort is the mechanism backing @template directives. Those directives can use the viewport to dynamically create, attach and detach views.
1 parent 9a28fa8 commit c6f14dd

File tree

9 files changed

+403
-30
lines changed

9 files changed

+403
-30
lines changed

modules/core/src/compiler/element_injector.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {List, ListWrapper} from 'facade/collection';
44
import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'di/di';
55
import {Parent, Ancestor} from 'core/annotations/visibility';
66
import {View} from 'core/compiler/view';
7+
import {ViewPort} from 'core/compiler/viewport';
78
import {NgElement} from 'core/dom/element';
89

910
var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10;
@@ -17,10 +18,12 @@ var _staticKeys;
1718
class StaticKeys {
1819
viewId:int;
1920
ngElementId:int;
21+
viewPortId:int;
2022
constructor() {
2123
//TODO: vsavkin Key.annotate(Key.get(View), 'static')
2224
this.viewId = Key.get(View).id;
2325
this.ngElementId = Key.get(NgElement).id;
26+
this.viewPortId = Key.get(ViewPort).id;
2427
}
2528

2629
static instance() {
@@ -61,6 +64,10 @@ class TreeNode {
6164
return this._parent;
6265
}
6366

67+
set parent(node:TreeNode) {
68+
this._parent = node;
69+
}
70+
6471
get children() {
6572
var res = [];
6673
var child = this._head;
@@ -92,12 +99,16 @@ class DirectiveDependency extends Dependency {
9299
}
93100
}
94101

102+
103+
// TODO(rado): benchmark and consider rolling in as ElementInjector fields.
95104
export class PreBuiltObjects {
96105
view:View;
97106
element:NgElement;
98-
constructor(view:View, element:NgElement) {
107+
viewPort:ViewPort;
108+
constructor(view, element:NgElement, viewPort: ViewPort) {
99109
this.view = view;
100110
this.element = element;
111+
this.viewPort = viewPort;
101112
}
102113
}
103114

@@ -410,6 +421,11 @@ export class ElementInjector extends TreeNode {
410421
var staticKeys = StaticKeys.instance();
411422
if (keyId === staticKeys.viewId) return this._preBuiltObjects.view;
412423
if (keyId === staticKeys.ngElementId) return this._preBuiltObjects.element;
424+
if (keyId === staticKeys.viewPortId) {
425+
if (isBlank(staticKeys.viewPortId)) throw new BaseException(
426+
'ViewPort is constructed only for @Template directives');
427+
return this._preBuiltObjects.viewPort;
428+
}
413429
//TODO add other objects as needed
414430
return _undefined;
415431
}

modules/core/src/compiler/view.js

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import {SetterFn} from 'reflection/types';
1111
import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang';
1212
import {Injector} from 'di/di';
1313
import {NgElement} from 'core/dom/element';
14+
import {ViewPort} from './viewport';
1415

1516
const NG_BINDING_CLASS = 'ng-binding';
1617

17-
/***
18+
/**
1819
* Const of making objects: http://jsperf.com/instantiate-size-of-object
1920
*/
2021
@IMPLEMENTS(WatchGroupDispatcher)
@@ -29,7 +30,8 @@ export class View {
2930
/// to keep track of the nodes.
3031
nodes:List<Node>;
3132
onChangeDispatcher:OnChangeDispatcher;
32-
childViews: List<View>;
33+
componentChildViews: List<View>;
34+
viewPorts: List<ViewPort>;
3335
constructor(nodes:List<Node>, elementInjectors:List,
3436
rootElementInjectors:List, textNodes:List, bindElements:List,
3537
protoRecordRange:ProtoRecordRange, context) {
@@ -41,9 +43,8 @@ export class View {
4143
this.bindElements = bindElements;
4244
this.recordRange = protoRecordRange.instantiate(this, MapWrapper.create());
4345
this.recordRange.setContext(context);
44-
// TODO(rado): Since this is only used in tests for now, investigate whether
45-
// we can remove it.
46-
this.childViews = [];
46+
this.componentChildViews = null;
47+
this.viewPorts = null;
4748
}
4849

4950
onRecordChange(record:Record, target) {
@@ -62,10 +63,24 @@ export class View {
6263
}
6364
}
6465

65-
addChild(childView: View) {
66-
ListWrapper.push(this.childViews, childView);
66+
addViewPort(viewPort: ViewPort) {
67+
if (isBlank(this.viewPorts)) this.viewPorts = [];
68+
ListWrapper.push(this.viewPorts, viewPort);
69+
}
70+
71+
addComponentChildView(childView: View) {
72+
if (isBlank(this.componentChildViews)) this.componentChildViews = [];
73+
ListWrapper.push(this.componentChildViews, childView);
74+
this.recordRange.addRange(childView.recordRange);
75+
}
76+
77+
addViewPortChildView(childView: View) {
6778
this.recordRange.addRange(childView.recordRange);
6879
}
80+
81+
removeViewPortChildView(childView: View) {
82+
childView.recordRange.remove();
83+
}
6984
}
7085

7186
export class ProtoView {
@@ -120,9 +135,10 @@ export class ProtoView {
120135
bindElements, this.protoRecordRange, context);
121136

122137
ProtoView._instantiateDirectives(
123-
view, elements, elementInjectors, lightDomAppInjector, shadowAppInjectors);
124-
ProtoView._instantiateChildComponentViews(
125-
elements, binders, elementInjectors, shadowAppInjectors, view);
138+
view, elements, binders, elementInjectors, lightDomAppInjector,
139+
shadowAppInjectors, hostElementInjector);
140+
ProtoView._instantiateChildComponentViews(view, elements, binders,
141+
elementInjectors, shadowAppInjectors);
126142

127143
return view;
128144
}
@@ -212,12 +228,25 @@ export class ProtoView {
212228
}
213229

214230
static _instantiateDirectives(
215-
view: View, elements:List, injectors:List<ElementInjectors>, lightDomAppInjector: Injector,
216-
shadowDomAppInjectors:List<Injectors>) {
231+
view, elements:List, binders: List<ElementBinder>, injectors:List<ElementInjectors>,
232+
lightDomAppInjector: Injector, shadowDomAppInjectors:List<Injectors>,
233+
hostElementInjector: ElementInjector) {
217234
for (var i = 0; i < injectors.length; ++i) {
218-
var preBuiltObjs = new PreBuiltObjects(view, new NgElement(elements[i]));
219-
if (injectors[i] != null) injectors[i].instantiateDirectives(
235+
var injector = injectors[i];
236+
if (injector != null) {
237+
var binder = binders[i];
238+
var element = elements[i];
239+
var ngElement = new NgElement(element);
240+
var viewPort = null;
241+
if (isPresent(binder.templateDirective)) {
242+
viewPort = new ViewPort(view, element, binder.nestedProtoView, injector);
243+
viewPort.attach(lightDomAppInjector, hostElementInjector);
244+
view.addViewPort(viewPort);
245+
}
246+
var preBuiltObjs = new PreBuiltObjects(view, ngElement, viewPort);
247+
injector.instantiateDirectives(
220248
lightDomAppInjector, shadowDomAppInjectors[i], preBuiltObjs);
249+
}
221250
}
222251
}
223252

@@ -252,20 +281,17 @@ export class ProtoView {
252281
}
253282
}
254283

255-
static _instantiateChildComponentViews(elements, binders, injectors,
256-
shadowDomAppInjectors: List<Injector>, view: View) {
284+
static _instantiateChildComponentViews(view: View, elements, binders,
285+
injectors, shadowDomAppInjectors: List<Injector>) {
257286
for (var i = 0; i < binders.length; ++i) {
258287
var binder = binders[i];
259288
if (isPresent(binder.componentDirective)) {
260289
var injector = injectors[i];
261290
var childView = binder.nestedProtoView.instantiate(
262291
injector.getComponent(), shadowDomAppInjectors[i], injector);
263-
view.addChild(childView);
292+
view.addComponentChildView(childView);
264293
var shadowRoot = elements[i].createShadowRoot();
265-
// TODO(rado): reuse utility from ViewPort/View.
266-
for (var j = 0; j < childView.nodes.length; ++j) {
267-
DOM.appendChild(shadowRoot, childView.nodes[j]);
268-
}
294+
ViewPort.moveViewNodesIntoParent(shadowRoot, childView);
269295
}
270296
}
271297
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {View, ProtoView} from './view';
2+
import {DOM, Node, Element} from 'facade/dom';
3+
import {ListWrapper, MapWrapper, List} from 'facade/collection';
4+
import {BaseException} from 'facade/lang';
5+
import {Injector} from 'di/di';
6+
import {ElementInjector} from 'core/compiler/element_injector';
7+
import {isPresent, isBlank} from 'facade/lang';
8+
9+
export class ViewPort {
10+
parentView: View;
11+
templateElement: Element;
12+
defaultProtoView: ProtoView;
13+
_views: List<View>;
14+
_viewLastNode: List<Node>;
15+
elementInjector: ElementInjector;
16+
appInjector: Injector;
17+
hostElementInjector: ElementInjector;
18+
19+
constructor(parentView: View, templateElement: Element, defaultProtoView: ProtoView,
20+
elementInjector: ElementInjector) {
21+
this.parentView = parentView;
22+
this.templateElement = templateElement;
23+
this.defaultProtoView = defaultProtoView;
24+
this.elementInjector = elementInjector;
25+
26+
// The order in this list matches the DOM order.
27+
this._views = [];
28+
this.appInjector = null;
29+
this.hostElementInjector = null;
30+
}
31+
32+
attach(appInjector: Injector, hostElementInjector: ElementInjector) {
33+
this.appInjector = appInjector;
34+
this.hostElementInjector = hostElementInjector;
35+
}
36+
37+
detach() {
38+
this.appInjector = null;
39+
this.hostElementInjector = null;
40+
}
41+
42+
get(index: number): View {
43+
return this._views[index];
44+
}
45+
46+
get length() {
47+
return this._views.length;
48+
}
49+
50+
_siblingToInsertAfter(index: number) {
51+
if (index == 0) return this.templateElement;
52+
return ListWrapper.last(this._views[index - 1].nodes);
53+
}
54+
55+
get detached() {
56+
return isBlank(this.appInjector);
57+
}
58+
59+
// TODO(rado): profile and decide whether bounds checks should be added
60+
// to the methods below.
61+
create(atIndex=-1): View {
62+
if (this.detached) throw new BaseException(
63+
'Cannot create views on a detached view port');
64+
// TODO(rado): replace curried defaultProtoView.instantiate(appInjector,
65+
// hostElementInjector) with ViewFactory.
66+
var newView = this.defaultProtoView.instantiate(
67+
null, this.appInjector, this.hostElementInjector);
68+
return this.insert(newView, atIndex);
69+
}
70+
71+
insert(view, atIndex=-1): View {
72+
if (atIndex == -1) atIndex = this._views.length;
73+
ListWrapper.insert(this._views, atIndex, view);
74+
ViewPort.moveViewNodesAfterSibling(this._siblingToInsertAfter(atIndex), view);
75+
this.parentView.addViewPortChildView(view);
76+
this._linkElementInjectors(view);
77+
return view;
78+
}
79+
80+
remove(atIndex=-1): View {
81+
if (atIndex == -1) atIndex = this._views.length - 1;
82+
var removedView = this.get(atIndex);
83+
ListWrapper.removeAt(this._views, atIndex);
84+
ViewPort.removeViewNodesFromParent(this.templateElement.parentNode, removedView);
85+
this.parentView.removeViewPortChildView(removedView);
86+
this._unlinkElementInjectors(removedView);
87+
return removedView;
88+
}
89+
90+
_linkElementInjectors(view) {
91+
for (var i = 0; i < view.rootElementInjectors.length; ++i) {
92+
view.rootElementInjectors[i].parent = this.elementInjector;
93+
}
94+
}
95+
96+
_unlinkElementInjectors(view) {
97+
for (var i = 0; i < view.rootElementInjectors.length; ++i) {
98+
view.rootElementInjectors[i].parent = null;
99+
}
100+
}
101+
102+
static moveViewNodesIntoParent(parent, view) {
103+
for (var i = 0; i < view.nodes.length; ++i) {
104+
DOM.appendChild(parent, view.nodes[i]);
105+
}
106+
}
107+
108+
static moveViewNodesAfterSibling(sibling, view) {
109+
for (var i = view.nodes.length - 1; i >= 0; --i) {
110+
DOM.insertAfter(sibling, view.nodes[i]);
111+
}
112+
}
113+
114+
static removeViewNodesFromParent(parent, view) {
115+
for (var i = view.nodes.length - 1; i >= 0; --i) {
116+
DOM.removeChild(parent, view.nodes[i]);
117+
}
118+
}
119+
}

modules/core/test/compiler/element_injector_spec.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {Parent, Ancestor} from 'core/annotations/visibility';
66
import {Injector, Inject, bind} from 'di/di';
77
import {View} from 'core/compiler/view';
88
import {ProtoRecordRange} from 'change_detection/record_range';
9+
import {ViewPort} from 'core/compiler/viewport';
910
import {NgElement} from 'core/dom/element';
1011

1112
//TODO: vsavkin: use a spy object
@@ -66,7 +67,7 @@ class NeedsView {
6667
}
6768

6869
export function main() {
69-
var defaultPreBuiltObjects = new PreBuiltObjects(null, null);
70+
var defaultPreBuiltObjects = new PreBuiltObjects(null, null, null);
7071

7172
function humanize(tree, names:List) {
7273
var lookupName = (item) =>
@@ -177,7 +178,7 @@ export function main() {
177178

178179
it("should instantiate directives that depend on pre built objects", function () {
179180
var view = new DummyView();
180-
var inj = injector([NeedsView], null, null, new PreBuiltObjects(view, null));
181+
var inj = injector([NeedsView], null, null, new PreBuiltObjects(view, null, null));
181182

182183
expect(inj.get(NeedsView).view).toBe(view);
183184
});
@@ -282,17 +283,24 @@ export function main() {
282283
describe("pre built objects", function () {
283284
it("should return view", function () {
284285
var view = new DummyView();
285-
var inj = injector([], null, null, new PreBuiltObjects(view, null));
286+
var inj = injector([], null, null, new PreBuiltObjects(view, null, null));
286287

287288
expect(inj.get(View)).toEqual(view);
288289
});
289290

290291
it("should return element", function () {
291292
var element = new NgElement(null);
292-
var inj = injector([], null, null, new PreBuiltObjects(null, element));
293+
var inj = injector([], null, null, new PreBuiltObjects(null, element, null));
293294

294295
expect(inj.get(NgElement)).toEqual(element);
295296
});
297+
298+
it('should return viewPort', function () {
299+
var viewPort = new ViewPort(null, null, null, null);
300+
var inj = injector([], null, null, new PreBuiltObjects(null, null, viewPort));
301+
302+
expect(inj.get(ViewPort)).toEqual(viewPort);
303+
});
296304
});
297305
});
298306
}

0 commit comments

Comments
 (0)