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.
This commit is contained in:
Rado Kirov
2014-11-21 15:13:01 -08:00
parent 9a28fa8590
commit c6f14dd833
9 changed files with 403 additions and 30 deletions

View File

@ -4,6 +4,7 @@ import {List, ListWrapper} from 'facade/collection';
import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'di/di';
import {Parent, Ancestor} from 'core/annotations/visibility';
import {View} from 'core/compiler/view';
import {ViewPort} from 'core/compiler/viewport';
import {NgElement} from 'core/dom/element';
var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10;
@ -17,10 +18,12 @@ var _staticKeys;
class StaticKeys {
viewId:int;
ngElementId:int;
viewPortId:int;
constructor() {
//TODO: vsavkin Key.annotate(Key.get(View), 'static')
this.viewId = Key.get(View).id;
this.ngElementId = Key.get(NgElement).id;
this.viewPortId = Key.get(ViewPort).id;
}
static instance() {
@ -61,6 +64,10 @@ class TreeNode {
return this._parent;
}
set parent(node:TreeNode) {
this._parent = node;
}
get children() {
var res = [];
var child = this._head;
@ -92,12 +99,16 @@ class DirectiveDependency extends Dependency {
}
}
// TODO(rado): benchmark and consider rolling in as ElementInjector fields.
export class PreBuiltObjects {
view:View;
element:NgElement;
constructor(view:View, element:NgElement) {
viewPort:ViewPort;
constructor(view, element:NgElement, viewPort: ViewPort) {
this.view = view;
this.element = element;
this.viewPort = viewPort;
}
}
@ -410,6 +421,11 @@ export class ElementInjector extends TreeNode {
var staticKeys = StaticKeys.instance();
if (keyId === staticKeys.viewId) return this._preBuiltObjects.view;
if (keyId === staticKeys.ngElementId) return this._preBuiltObjects.element;
if (keyId === staticKeys.viewPortId) {
if (isBlank(staticKeys.viewPortId)) throw new BaseException(
'ViewPort is constructed only for @Template directives');
return this._preBuiltObjects.viewPort;
}
//TODO add other objects as needed
return _undefined;
}

View File

@ -11,10 +11,11 @@ import {SetterFn} from 'reflection/types';
import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang';
import {Injector} from 'di/di';
import {NgElement} from 'core/dom/element';
import {ViewPort} from './viewport';
const NG_BINDING_CLASS = 'ng-binding';
/***
/**
* Const of making objects: http://jsperf.com/instantiate-size-of-object
*/
@IMPLEMENTS(WatchGroupDispatcher)
@ -29,7 +30,8 @@ export class View {
/// to keep track of the nodes.
nodes:List<Node>;
onChangeDispatcher:OnChangeDispatcher;
childViews: List<View>;
componentChildViews: List<View>;
viewPorts: List<ViewPort>;
constructor(nodes:List<Node>, elementInjectors:List,
rootElementInjectors:List, textNodes:List, bindElements:List,
protoRecordRange:ProtoRecordRange, context) {
@ -41,9 +43,8 @@ export class View {
this.bindElements = bindElements;
this.recordRange = protoRecordRange.instantiate(this, MapWrapper.create());
this.recordRange.setContext(context);
// TODO(rado): Since this is only used in tests for now, investigate whether
// we can remove it.
this.childViews = [];
this.componentChildViews = null;
this.viewPorts = null;
}
onRecordChange(record:Record, target) {
@ -62,10 +63,24 @@ export class View {
}
}
addChild(childView: View) {
ListWrapper.push(this.childViews, childView);
addViewPort(viewPort: ViewPort) {
if (isBlank(this.viewPorts)) this.viewPorts = [];
ListWrapper.push(this.viewPorts, viewPort);
}
addComponentChildView(childView: View) {
if (isBlank(this.componentChildViews)) this.componentChildViews = [];
ListWrapper.push(this.componentChildViews, childView);
this.recordRange.addRange(childView.recordRange);
}
addViewPortChildView(childView: View) {
this.recordRange.addRange(childView.recordRange);
}
removeViewPortChildView(childView: View) {
childView.recordRange.remove();
}
}
export class ProtoView {
@ -120,9 +135,10 @@ export class ProtoView {
bindElements, this.protoRecordRange, context);
ProtoView._instantiateDirectives(
view, elements, elementInjectors, lightDomAppInjector, shadowAppInjectors);
ProtoView._instantiateChildComponentViews(
elements, binders, elementInjectors, shadowAppInjectors, view);
view, elements, binders, elementInjectors, lightDomAppInjector,
shadowAppInjectors, hostElementInjector);
ProtoView._instantiateChildComponentViews(view, elements, binders,
elementInjectors, shadowAppInjectors);
return view;
}
@ -212,12 +228,25 @@ export class ProtoView {
}
static _instantiateDirectives(
view: View, elements:List, injectors:List<ElementInjectors>, lightDomAppInjector: Injector,
shadowDomAppInjectors:List<Injectors>) {
view, elements:List, binders: List<ElementBinder>, injectors:List<ElementInjectors>,
lightDomAppInjector: Injector, shadowDomAppInjectors:List<Injectors>,
hostElementInjector: ElementInjector) {
for (var i = 0; i < injectors.length; ++i) {
var preBuiltObjs = new PreBuiltObjects(view, new NgElement(elements[i]));
if (injectors[i] != null) injectors[i].instantiateDirectives(
var injector = injectors[i];
if (injector != null) {
var binder = binders[i];
var element = elements[i];
var ngElement = new NgElement(element);
var viewPort = null;
if (isPresent(binder.templateDirective)) {
viewPort = new ViewPort(view, element, binder.nestedProtoView, injector);
viewPort.attach(lightDomAppInjector, hostElementInjector);
view.addViewPort(viewPort);
}
var preBuiltObjs = new PreBuiltObjects(view, ngElement, viewPort);
injector.instantiateDirectives(
lightDomAppInjector, shadowDomAppInjectors[i], preBuiltObjs);
}
}
}
@ -252,20 +281,17 @@ export class ProtoView {
}
}
static _instantiateChildComponentViews(elements, binders, injectors,
shadowDomAppInjectors: List<Injector>, view: View) {
static _instantiateChildComponentViews(view: View, elements, binders,
injectors, shadowDomAppInjectors: List<Injector>) {
for (var i = 0; i < binders.length; ++i) {
var binder = binders[i];
if (isPresent(binder.componentDirective)) {
var injector = injectors[i];
var childView = binder.nestedProtoView.instantiate(
injector.getComponent(), shadowDomAppInjectors[i], injector);
view.addChild(childView);
view.addComponentChildView(childView);
var shadowRoot = elements[i].createShadowRoot();
// TODO(rado): reuse utility from ViewPort/View.
for (var j = 0; j < childView.nodes.length; ++j) {
DOM.appendChild(shadowRoot, childView.nodes[j]);
}
ViewPort.moveViewNodesIntoParent(shadowRoot, childView);
}
}
}

View File

@ -0,0 +1,119 @@
import {View, ProtoView} from './view';
import {DOM, Node, Element} from 'facade/dom';
import {ListWrapper, MapWrapper, List} from 'facade/collection';
import {BaseException} from 'facade/lang';
import {Injector} from 'di/di';
import {ElementInjector} from 'core/compiler/element_injector';
import {isPresent, isBlank} from 'facade/lang';
export class ViewPort {
parentView: View;
templateElement: Element;
defaultProtoView: ProtoView;
_views: List<View>;
_viewLastNode: List<Node>;
elementInjector: ElementInjector;
appInjector: Injector;
hostElementInjector: ElementInjector;
constructor(parentView: View, templateElement: Element, defaultProtoView: ProtoView,
elementInjector: ElementInjector) {
this.parentView = parentView;
this.templateElement = templateElement;
this.defaultProtoView = defaultProtoView;
this.elementInjector = elementInjector;
// The order in this list matches the DOM order.
this._views = [];
this.appInjector = null;
this.hostElementInjector = null;
}
attach(appInjector: Injector, hostElementInjector: ElementInjector) {
this.appInjector = appInjector;
this.hostElementInjector = hostElementInjector;
}
detach() {
this.appInjector = null;
this.hostElementInjector = null;
}
get(index: number): View {
return this._views[index];
}
get length() {
return this._views.length;
}
_siblingToInsertAfter(index: number) {
if (index == 0) return this.templateElement;
return ListWrapper.last(this._views[index - 1].nodes);
}
get detached() {
return isBlank(this.appInjector);
}
// TODO(rado): profile and decide whether bounds checks should be added
// to the methods below.
create(atIndex=-1): View {
if (this.detached) throw new BaseException(
'Cannot create views on a detached view port');
// TODO(rado): replace curried defaultProtoView.instantiate(appInjector,
// hostElementInjector) with ViewFactory.
var newView = this.defaultProtoView.instantiate(
null, this.appInjector, this.hostElementInjector);
return this.insert(newView, atIndex);
}
insert(view, atIndex=-1): View {
if (atIndex == -1) atIndex = this._views.length;
ListWrapper.insert(this._views, atIndex, view);
ViewPort.moveViewNodesAfterSibling(this._siblingToInsertAfter(atIndex), view);
this.parentView.addViewPortChildView(view);
this._linkElementInjectors(view);
return view;
}
remove(atIndex=-1): View {
if (atIndex == -1) atIndex = this._views.length - 1;
var removedView = this.get(atIndex);
ListWrapper.removeAt(this._views, atIndex);
ViewPort.removeViewNodesFromParent(this.templateElement.parentNode, removedView);
this.parentView.removeViewPortChildView(removedView);
this._unlinkElementInjectors(removedView);
return removedView;
}
_linkElementInjectors(view) {
for (var i = 0; i < view.rootElementInjectors.length; ++i) {
view.rootElementInjectors[i].parent = this.elementInjector;
}
}
_unlinkElementInjectors(view) {
for (var i = 0; i < view.rootElementInjectors.length; ++i) {
view.rootElementInjectors[i].parent = null;
}
}
static moveViewNodesIntoParent(parent, view) {
for (var i = 0; i < view.nodes.length; ++i) {
DOM.appendChild(parent, view.nodes[i]);
}
}
static moveViewNodesAfterSibling(sibling, view) {
for (var i = view.nodes.length - 1; i >= 0; --i) {
DOM.insertAfter(sibling, view.nodes[i]);
}
}
static removeViewNodesFromParent(parent, view) {
for (var i = view.nodes.length - 1; i >= 0; --i) {
DOM.removeChild(parent, view.nodes[i]);
}
}
}