feat(render): add initial implementation of render layer

This commit is contained in:
Tobias Bosch
2015-03-23 14:10:55 -07:00
parent 814d389b6e
commit 6c60c3e547
64 changed files with 7248 additions and 1 deletions

View File

@ -0,0 +1,59 @@
import {AST} from 'angular2/change_detection';
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import * as protoViewModule from './proto_view';
/**
* Note: Code that uses this class assumes that is immutable!
*/
export class ElementBinder {
contentTagSelector: string;
textNodeIndices: List<number>;
nestedProtoView: protoViewModule.ProtoView;
eventLocals: AST;
eventNames: List<string>;
componentId: string;
parentIndex:number;
distanceToParent:number;
constructor({
textNodeIndices,
contentTagSelector,
nestedProtoView,
componentId,
eventLocals,
eventNames,
parentIndex,
distanceToParent
}) {
this.textNodeIndices = textNodeIndices;
this.contentTagSelector = contentTagSelector;
this.nestedProtoView = nestedProtoView;
this.componentId = componentId;
this.eventLocals = eventLocals;
this.eventNames = eventNames;
this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent;
}
mergeChildComponentProtoViews(protoViews:List<protoViewModule.ProtoView>, target:List<protoViewModule.ProtoView>):ElementBinder {
var nestedProtoView;
if (isPresent(this.componentId)) {
nestedProtoView = ListWrapper.removeAt(protoViews, 0);
} else if (isPresent(this.nestedProtoView)) {
nestedProtoView = this.nestedProtoView.mergeChildComponentProtoViews(protoViews, target);
}
return new ElementBinder({
parentIndex: this.parentIndex,
// Don't clone as we assume immutability!
textNodeIndices: this.textNodeIndices,
contentTagSelector: this.contentTagSelector,
nestedProtoView: nestedProtoView,
componentId: this.componentId,
// Don't clone as we assume immutability!
eventLocals: this.eventLocals,
eventNames: this.eventNames,
distanceToParent: this.distanceToParent
});
}
}

View File

@ -0,0 +1,55 @@
import {isPresent} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {SetterFn} from 'angular2/src/reflection/types';
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {ElementBinder} from './element_binder';
import {NG_BINDING_CLASS} from '../util';
/**
* Note: Code that uses this class assumes that is immutable!
*/
export class ProtoView {
element;
elementBinders:List<ElementBinder>;
isTemplateElement:boolean;
isRootView:boolean;
rootBindingOffset:int;
propertySetters: Map<string, SetterFn>;
constructor({
elementBinders,
element,
isRootView,
propertySetters
}) {
this.element = element;
this.elementBinders = elementBinders;
this.isTemplateElement = DOM.isTemplateElement(this.element);
this.isRootView = isRootView;
this.rootBindingOffset = (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0;
this.propertySetters = propertySetters;
}
mergeChildComponentProtoViews(protoViews:List<ProtoView>, target:List<ProtoView>):ProtoView {
var elementBinders = ListWrapper.createFixedSize(this.elementBinders.length);
for (var i=0; i<this.elementBinders.length; i++) {
var eb = this.elementBinders[i];
if (isPresent(eb.componentId) || isPresent(eb.nestedProtoView)) {
elementBinders[i] = eb.mergeChildComponentProtoViews(protoViews, target);
} else {
elementBinders[i] = eb;
}
}
var result = new ProtoView({
elementBinders: elementBinders,
element: this.element,
isRootView: this.isRootView,
// Don't clone as we assume immutability!
propertySetters: this.propertySetters
});
ListWrapper.insert(target, 0, result);
return result
}
}

View File

@ -0,0 +1,297 @@
import {isPresent, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, Set, SetWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
ASTWithSource, AST, AstTransformer, AccessMember, LiteralArray, ImplicitReceiver
} from 'angular2/change_detection';
import {SetterFn} from 'angular2/src/reflection/types';
import {ProtoView} from './proto_view';
import {ElementBinder} from './element_binder';
import * as api from '../../api';
import * as directDomRenderer from '../direct_dom_renderer';
import {NG_BINDING_CLASS} from '../util';
export class ProtoViewBuilder {
rootElement;
variableBindings: Map<string, string>;
elements:List<ElementBinderBuilder>;
isRootView:boolean;
propertySetters:Set<string>;
constructor(rootElement) {
this.rootElement = rootElement;
this.elements = [];
this.isRootView = false;
this.variableBindings = MapWrapper.create();
this.propertySetters = new Set();
}
bindElement(element, description = null):ElementBinderBuilder {
var builder = new ElementBinderBuilder(this.elements.length, element, description);
ListWrapper.push(this.elements, builder);
DOM.addClass(element, NG_BINDING_CLASS);
return builder;
}
bindVariable(name, value) {
// Store the variable map from value to variable, reflecting how it will be used later by
// View. When a local is set to the view, a lookup for the variable name will take place keyed
// by the "value", or exported identifier. For example, ng-repeat sets a view local of "index".
// When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
// it.
MapWrapper.set(this.variableBindings, value, name);
}
setIsRootView(value) {
this.isRootView = value;
}
build():api.ProtoView {
var renderElementBinders = [];
var apiElementBinders = [];
var propertySetters = MapWrapper.create();
ListWrapper.forEach(this.elements, (ebb) => {
var eventLocalsAstSplitter = new EventLocalsAstSplitter();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => {
MapWrapper.forEach(db.propertySetters, (setter, propertyName) => {
MapWrapper.set(propertySetters, propertyName, setter);
});
return new api.DirectiveBinder({
directiveIndex: db.directiveIndex,
propertyBindings: db.propertyBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(db.eventBindings)
});
});
MapWrapper.forEach(ebb.propertySetters, (setter, propertyName) => {
MapWrapper.set(propertySetters, propertyName, setter);
});
var nestedProtoView =
isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null;
var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1;
var parentWithDirectivesIndex = isPresent(ebb.parentWithDirectives) ? ebb.parentWithDirectives.index : -1;
ListWrapper.push(apiElementBinders, new api.ElementBinder({
index: ebb.index, parentIndex:parentIndex, distanceToParent:ebb.distanceToParent,
parentWithDirectivesIndex: parentWithDirectivesIndex, distanceToParentWithDirectives: ebb.distanceToParentWithDirectives,
directives: apiDirectiveBinders,
nestedProtoView: nestedProtoView,
propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(ebb.eventBindings),
textBindings: ebb.textBindings
}));
ListWrapper.push(renderElementBinders, new ElementBinder({
textNodeIndices: ebb.textBindingIndices,
contentTagSelector: ebb.contentTagSelector,
parentIndex: parentIndex,
distanceToParent: ebb.distanceToParent,
nestedProtoView: isPresent(nestedProtoView) ? nestedProtoView.render.delegate : null,
componentId: ebb.componentId,
eventLocals: eventLocalsAstSplitter.buildEventLocals(),
eventNames: eventLocalsAstSplitter.buildEventNames()
}));
});
return new api.ProtoView({
render: new directDomRenderer.DirectDomProtoViewRef(new ProtoView({
element: this.rootElement,
elementBinders: renderElementBinders,
isRootView: this.isRootView,
propertySetters: propertySetters
})),
elementBinders: apiElementBinders,
variableBindings: this.variableBindings
});
}
}
export class ElementBinderBuilder {
element;
index:number;
parent:ElementBinderBuilder;
distanceToParent:number;
parentWithDirectives:ElementBinderBuilder;
distanceToParentWithDirectives:number;
directives:List<DirectiveBuilder>;
nestedProtoView:ProtoViewBuilder;
propertyBindings: Map<string, ASTWithSource>;
variableBindings: Map<string, string>;
eventBindings: Map<string, ASTWithSource>;
textBindingIndices: List<number>;
textBindings: List<ASTWithSource>;
contentTagSelector:string;
propertySetters: Map<string, SetterFn>;
componentId: string;
constructor(index, element, description) {
this.element = element;
this.index = index;
this.parent = null;
this.distanceToParent = 0;
this.parentWithDirectives = null;
this.distanceToParentWithDirectives = 0;
this.directives = [];
this.nestedProtoView = null;
this.propertyBindings = MapWrapper.create();
this.variableBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create();
this.textBindings = [];
this.textBindingIndices = [];
this.contentTagSelector = null;
this.propertySetters = MapWrapper.create();
this.componentId = null;
}
setParent(parent:ElementBinderBuilder, distanceToParent):ElementBinderBuilder {
this.parent = parent;
if (isPresent(parent)) {
this.distanceToParent = distanceToParent;
if (parent.directives.length > 0) {
this.parentWithDirectives = parent;
this.distanceToParentWithDirectives = distanceToParent;
} else {
this.parentWithDirectives = parent.parentWithDirectives;
if (isPresent(this.parentWithDirectives)) {
this.distanceToParentWithDirectives = distanceToParent + parent.distanceToParentWithDirectives;
}
}
}
return this;
}
bindDirective(directiveIndex:number):DirectiveBuilder {
var directive = new DirectiveBuilder(directiveIndex);
ListWrapper.push(this.directives, directive);
return directive;
}
bindNestedProtoView(rootElement):ProtoViewBuilder {
if (isPresent(this.nestedProtoView)) {
throw new BaseException('Only one nested view per element is allowed');
}
this.nestedProtoView = new ProtoViewBuilder(rootElement);
return this.nestedProtoView;
}
bindProperty(name, expression) {
MapWrapper.set(this.propertyBindings, name, expression);
}
bindVariable(name, value) {
// When current is a view root, the variable bindings are set to the *nested* proto view.
// The root view conceptually signifies a new "block scope" (the nested view), to which
// the variables are bound.
if (isPresent(this.nestedProtoView)) {
this.nestedProtoView.bindVariable(name, value);
} else {
// Store the variable map from value to variable, reflecting how it will be used later by
// View. When a local is set to the view, a lookup for the variable name will take place keyed
// by the "value", or exported identifier. For example, ng-repeat sets a view local of "index".
// When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
// it.
MapWrapper.set(this.variableBindings, value, name);
}
}
bindEvent(name, expression) {
MapWrapper.set(this.eventBindings, name, expression);
}
bindText(index, expression) {
ListWrapper.push(this.textBindingIndices, index);
ListWrapper.push(this.textBindings, expression);
}
setContentTagSelector(value:string) {
this.contentTagSelector = value;
}
bindPropertySetter(propertyName, setter) {
MapWrapper.set(this.propertySetters, propertyName, setter);
}
setComponentId(componentId:string) {
this.componentId = componentId;
}
}
export class DirectiveBuilder {
directiveIndex:number;
propertyBindings: Map<string, ASTWithSource>;
eventBindings: Map<string, ASTWithSource>;
propertySetters: Map<string, SetterFn>;
constructor(directiveIndex) {
this.directiveIndex = directiveIndex;
this.propertyBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create();
this.propertySetters = MapWrapper.create();
}
bindProperty(name, expression) {
MapWrapper.set(this.propertyBindings, name, expression);
}
bindEvent(name, expression) {
MapWrapper.set(this.eventBindings, name, expression);
}
bindPropertySetter(propertyName, setter) {
MapWrapper.set(this.propertySetters, propertyName, setter);
}
}
export class EventLocalsAstSplitter extends AstTransformer {
locals:List<AST>;
eventNames:List<string>;
_implicitReceiver:AST;
constructor() {
super();
this.locals = [];
this.eventNames = [];
this._implicitReceiver = new ImplicitReceiver();
}
splitEventAstIntoLocals(eventBindings:Map<string, ASTWithSource>):Map<string, ASTWithSource> {
if (isPresent(eventBindings)) {
var result = MapWrapper.create();
MapWrapper.forEach(eventBindings, (astWithSource, eventName) => {
MapWrapper.set(result, eventName, astWithSource.ast.visit(this));
ListWrapper.push(this.eventNames, eventName);
});
return result;
}
return null;
}
visitAccessMember(ast:AccessMember) {
var isEventAccess = false;
var current = ast;
while (!isEventAccess && (current instanceof AccessMember)) {
if (current.name == '$event') {
isEventAccess = true;
}
current = current.receiver;
}
if (isEventAccess) {
ListWrapper.push(this.locals, ast);
var index = this.locals.length - 1;
return new AccessMember(this._implicitReceiver, `${index}`, (arr) => arr[index], null);
} else {
return ast;
}
}
buildEventLocals() {
return new LiteralArray(this.locals);
}
buildEventNames() {
return this.eventNames;
}
}

View File

@ -0,0 +1,168 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {int, isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {Locals} from 'angular2/change_detection';
import {ViewContainer} from './view_container';
import {ProtoView} from './proto_view';
import {LightDom} from '../shadow_dom/light_dom';
import {Content} from '../shadow_dom/content_tag';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import {EventDispatcher} from '../../api';
const NG_BINDING_CLASS = 'ng-binding';
/**
* Const of making objects: http://jsperf.com/instantiate-size-of-object
*/
export class View {
boundElements:List;
boundTextNodes:List;
/// When the view is part of render tree, the DocumentFragment is empty, which is why we need
/// to keep track of the nodes.
rootNodes:List;
// TODO(tbosch): move componentChildViews, viewContainers, contentTags, lightDoms into
// a single array with records inside
componentChildViews: List<View>;
viewContainers: List<ViewContainer>;
contentTags: List<Content>;
lightDoms: List<LightDom>;
proto: ProtoView;
_hydrated: boolean;
_eventDispatcher: EventDispatcher;
constructor(
proto:ProtoView, rootNodes:List,
boundTextNodes: List, boundElements:List, viewContainers:List, contentTags:List) {
this.proto = proto;
this.rootNodes = rootNodes;
this.boundTextNodes = boundTextNodes;
this.boundElements = boundElements;
this.viewContainers = viewContainers;
this.contentTags = contentTags;
this.lightDoms = ListWrapper.createFixedSize(boundElements.length);
this.componentChildViews = ListWrapper.createFixedSize(boundElements.length);
this._hydrated = false;
}
hydrated() {
return this._hydrated;
}
setElementProperty(elementIndex:number, propertyName:string, value:any) {
var setter = MapWrapper.get(this.proto.propertySetters, propertyName);
setter(this.boundElements[elementIndex], value);
}
setText(textIndex:number, value:string) {
DOM.setText(this.boundTextNodes[textIndex], value);
}
setComponentView(strategy: ShadowDomStrategy,
elementIndex:number, childView:View) {
var element = this.boundElements[elementIndex];
var lightDom = strategy.constructLightDom(this, childView, element);
strategy.attachTemplate(element, childView);
this.lightDoms[elementIndex] = lightDom;
this.componentChildViews[elementIndex] = childView;
if (this._hydrated) {
childView.hydrate(lightDom);
}
}
getViewContainer(index:number):ViewContainer {
return this.viewContainers[index];
}
_getDestLightDom(binderIndex) {
var binder = this.proto.elementBinders[binderIndex];
var destLightDom = null;
if (binder.parentIndex !== -1 && binder.distanceToParent === 1) {
destLightDom = this.lightDoms[binder.parentIndex];
}
return destLightDom;
}
/**
* A dehydrated view is a state of the view that allows it to be moved around
* the view tree.
*
* A dehydrated view has the following properties:
*
* - all viewcontainers are empty.
*
* A call to hydrate/dehydrate does not attach/detach the view from the view
* tree.
*/
hydrate(hostLightDom: LightDom) {
if (this._hydrated) throw new BaseException('The view is already hydrated.');
this._hydrated = true;
// viewContainers and content tags
for (var i = 0; i < this.viewContainers.length; i++) {
var vc = this.viewContainers[i];
var destLightDom = this._getDestLightDom(i);
if (isPresent(vc)) {
vc.hydrate(destLightDom, hostLightDom);
}
var ct = this.contentTags[i];
if (isPresent(ct)) {
ct.hydrate(destLightDom);
}
}
// componentChildViews
for (var i = 0; i < this.componentChildViews.length; i++) {
var cv = this.componentChildViews[i];
if (isPresent(cv)) {
cv.hydrate(this.lightDoms[i]);
}
}
for (var i = 0; i < this.lightDoms.length; ++i) {
var lightDom = this.lightDoms[i];
if (isPresent(lightDom)) {
lightDom.redistribute();
}
}
}
dehydrate() {
// Note: preserve the opposite order of the hydration process.
// componentChildViews
for (var i = 0; i < this.componentChildViews.length; i++) {
this.componentChildViews[i].dehydrate();
}
// viewContainers and content tags
if (isPresent(this.viewContainers)) {
for (var i = 0; i < this.viewContainers.length; i++) {
var vc = this.viewContainers[i];
if (isPresent(vc)) {
vc.dehydrate();
}
var ct = this.contentTags[i];
if (isPresent(ct)) {
ct.dehydrate();
}
}
}
this._hydrated = false;
}
setEventDispatcher(dispatcher:EventDispatcher) {
this._eventDispatcher = dispatcher;
}
dispatchEvent(elementIndex, eventName, event) {
if (isPresent(this._eventDispatcher)) {
var evalLocals = MapWrapper.create();
MapWrapper.set(evalLocals, '$event', event);
var localValues = this.proto.elementBinders[elementIndex].eventLocals.eval(null, new Locals(null, evalLocals));
this._eventDispatcher.dispatchEvent(elementIndex, eventName, localValues);
}
}
}

View File

@ -0,0 +1,137 @@
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, List} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import * as viewModule from './view';
import * as ldModule from '../shadow_dom/light_dom';
import * as vfModule from './view_factory';
export class ViewContainer {
_viewFactory: vfModule.ViewFactory;
templateElement;
_views: List<viewModule.View>;
_lightDom: ldModule.LightDom;
_hostLightDom: ldModule.LightDom;
_hydrated: boolean;
constructor(viewFactory: vfModule.ViewFactory,
templateElement) {
this._viewFactory = viewFactory;
this.templateElement = templateElement;
// The order in this list matches the DOM order.
this._views = [];
this._hostLightDom = null;
this._hydrated = false;
}
hydrate(destLightDom: ldModule.LightDom, hostLightDom: ldModule.LightDom) {
this._hydrated = true;
this._hostLightDom = hostLightDom;
this._lightDom = destLightDom;
}
dehydrate() {
if (isBlank(this._lightDom)) {
for (var i = this._views.length - 1; i >= 0; i--) {
var view = this._views[i];
ViewContainer.removeViewNodesFromParent(this.templateElement.parentNode, view);
this._viewFactory.returnView(view);
}
this._views = [];
} else {
for (var i=0; i<this._views.length; i++) {
var view = this._views[i];
this._viewFactory.returnView(view);
}
this._views = [];
this._lightDom.redistribute();
}
this._hostLightDom = null;
this._lightDom = null;
this._hydrated = false;
}
get(index: number): viewModule.View {
return this._views[index];
}
size() {
return this._views.length;
}
_siblingToInsertAfter(index: number) {
if (index == 0) return this.templateElement;
return ListWrapper.last(this._views[index - 1].rootNodes);
}
_checkHydrated() {
if (!this._hydrated) throw new BaseException(
'Cannot change dehydrated ViewContainer');
}
insert(view, atIndex=-1): viewModule.View {
this._checkHydrated();
if (atIndex == -1) atIndex = this._views.length;
ListWrapper.insert(this._views, atIndex, view);
if (!view.hydrated()) {
view.hydrate(this._hostLightDom);
}
if (isBlank(this._lightDom)) {
ViewContainer.moveViewNodesAfterSibling(this._siblingToInsertAfter(atIndex), view);
} else {
this._lightDom.redistribute();
}
// new content tags might have appeared, we need to redistribute.
if (isPresent(this._hostLightDom)) {
this._hostLightDom.redistribute();
}
return view;
}
/**
* The method can be used together with insert to implement a view move, i.e.
* moving the dom nodes while the directives in the view stay intact.
*/
detach(atIndex:number) {
this._checkHydrated();
var detachedView = this.get(atIndex);
ListWrapper.removeAt(this._views, atIndex);
if (isBlank(this._lightDom)) {
ViewContainer.removeViewNodesFromParent(this.templateElement.parentNode, detachedView);
} else {
this._lightDom.redistribute();
}
// content tags might have disappeared we need to do redistribution.
if (isPresent(this._hostLightDom)) {
this._hostLightDom.redistribute();
}
return detachedView;
}
contentTagContainers() {
return this._views;
}
nodes():List {
var r = [];
for (var i = 0; i < this._views.length; ++i) {
r = ListWrapper.concat(r, this._views[i].rootNodes);
}
return r;
}
static moveViewNodesAfterSibling(sibling, view) {
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
DOM.insertAfter(sibling, view.rootNodes[i]);
}
}
static removeViewNodesFromParent(parent, view) {
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
DOM.removeChild(parent, view.rootNodes[i]);
}
}
}

View File

@ -0,0 +1,158 @@
import {OpaqueToken} from 'angular2/di';
import {int, isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Content} from '../shadow_dom/content_tag';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import {EventManager} from '../events/event_manager';
import {ViewContainer} from './view_container';
import {ProtoView} from './proto_view';
import {View} from './view';
import {NG_BINDING_CLASS_SELECTOR, NG_BINDING_CLASS} from '../util';
export var VIEW_POOL_CAPACITY = new OpaqueToken('ViewFactory.viewPoolCapacity');
export class ViewFactory {
_poolCapacity:number;
_pooledViews:List<View>;
_eventManager:EventManager;
_shadowDomStrategy:ShadowDomStrategy;
constructor(capacity, eventManager, shadowDomStrategy) {
this._poolCapacity = capacity;
this._pooledViews = ListWrapper.create();
this._eventManager = eventManager;
this._shadowDomStrategy = shadowDomStrategy;
}
getView(protoView:ProtoView):View {
// TODO(tbosch): benchmark this scanning of views and maybe
// replace it with a fancy LRU Map/List combination...
var view;
for (var i=0; i<this._pooledViews.length; i++) {
var pooledView = this._pooledViews[i];
if (pooledView.proto === protoView) {
view = ListWrapper.removeAt(this._pooledViews, i);
}
}
if (isBlank(view)) {
view = this._createView(protoView);
}
return view;
}
returnView(view:View) {
if (view.hydrated()) {
view.dehydrate();
}
ListWrapper.push(this._pooledViews, view);
while (this._pooledViews.length > this._poolCapacity) {
ListWrapper.removeAt(this._pooledViews, 0);
}
}
_createView(protoView:ProtoView): View {
var rootElementClone = protoView.isRootView ? protoView.element : DOM.importIntoDoc(protoView.element);
var elementsWithBindingsDynamic;
if (protoView.isTemplateElement) {
elementsWithBindingsDynamic = DOM.querySelectorAll(DOM.content(rootElementClone), NG_BINDING_CLASS_SELECTOR);
} else {
elementsWithBindingsDynamic = DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS);
}
var elementsWithBindings = ListWrapper.createFixedSize(elementsWithBindingsDynamic.length);
for (var binderIdx = 0; binderIdx < elementsWithBindingsDynamic.length; ++binderIdx) {
elementsWithBindings[binderIdx] = elementsWithBindingsDynamic[binderIdx];
}
var viewRootNodes;
if (protoView.isTemplateElement) {
var childNode = DOM.firstChild(DOM.content(rootElementClone));
viewRootNodes = []; // TODO(perf): Should be fixed size, since we could pre-compute in in ProtoView
// Note: An explicit loop is the fastest way to convert a DOM array into a JS array!
while(childNode != null) {
ListWrapper.push(viewRootNodes, childNode);
childNode = DOM.nextSibling(childNode);
}
} else {
viewRootNodes = [rootElementClone];
}
var binders = protoView.elementBinders;
var boundTextNodes = [];
var boundElements = ListWrapper.createFixedSize(binders.length);
var viewContainers = ListWrapper.createFixedSize(binders.length);
var contentTags = ListWrapper.createFixedSize(binders.length);
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
var element;
if (binderIdx === 0 && protoView.rootBindingOffset === 1) {
element = rootElementClone;
} else {
element = elementsWithBindings[binderIdx - protoView.rootBindingOffset];
}
boundElements[binderIdx] = element;
// boundTextNodes
var childNodes = DOM.childNodes(DOM.templateAwareRoot(element));
var textNodeIndices = binder.textNodeIndices;
for (var i = 0; i<textNodeIndices.length; i++) {
ListWrapper.push(boundTextNodes, childNodes[textNodeIndices[i]]);
}
// viewContainers
var viewContainer = null;
if (isBlank(binder.componentId) && isPresent(binder.nestedProtoView)) {
viewContainer = new ViewContainer(this, element);
}
viewContainers[binderIdx] = viewContainer;
// contentTags
var contentTag = null;
if (isPresent(binder.contentTagSelector)) {
contentTag = new Content(element, binder.contentTagSelector);
}
contentTags[binderIdx] = contentTag;
}
var view = new View(
protoView, viewRootNodes,
boundTextNodes, boundElements, viewContainers, contentTags
);
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
var element = boundElements[binderIdx];
// static child components
if (isPresent(binder.componentId) && isPresent(binder.nestedProtoView)) {
var childView = this._createView(binder.nestedProtoView);
view.setComponentView(this._shadowDomStrategy, binderIdx, childView);
}
// events
if (isPresent(binder.eventLocals)) {
ListWrapper.forEach(binder.eventNames, (eventName) => {
this._createEventListener(view, element, binderIdx, eventName, binder.eventLocals);
});
}
}
if (protoView.isRootView) {
view.hydrate(null);
}
return view;
}
_createEventListener(view, element, elementIndex, eventName, eventLocals) {
this._eventManager.addEventListener(element, eventName, (event) => {
view.dispatchEvent(elementIndex, eventName, event);
});
}
}