Files
Angular-Cn/modules/angular2/src/core/compiler/view.js
T
Tobias Bosch ca958464c4 refactor(render): create and store render ProtoViewRef in every app ProtoView
Needed to change Renderer.mergeChildComponentProtoViews to not create
new ProtoViews to be able to deal with cyclic references.

This commit is part of using the new render layer in Angular.
2015-04-07 20:27:25 -07:00

710 lines
25 KiB
JavaScript

import {DOM} from 'angular2/src/dom/dom_adapter';
import {Promise} from 'angular2/src/facade/async';
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {AST, Locals, ChangeDispatcher, ProtoChangeDetector, ChangeDetector,
ChangeRecord, BindingRecord, BindingPropagationConfig, uninitialized} from 'angular2/change_detection';
import {ProtoElementInjector, ElementInjector, PreBuiltObjects, DirectiveBinding} from './element_injector';
import {ElementBinder} from './element_binder';
import {SetterFn} from 'angular2/src/reflection/types';
import {IMPLEMENTS, int, isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {Injector} from 'angular2/di';
import {NgElement} from 'angular2/src/core/dom/element';
import {ViewContainer} from './view_container';
import {LightDom} from './shadow_dom_emulation/light_dom';
import {Content} from './shadow_dom_emulation/content_tag';
import {ShadowDomStrategy} from './shadow_dom_strategy';
import {ViewPool} from './view_pool';
import {EventManager} from 'angular2/src/render/dom/events/event_manager';
import * as renderApi from 'angular2/src/render/api';
const NG_BINDING_CLASS = 'ng-binding';
const NG_BINDING_CLASS_SELECTOR = '.ng-binding';
// TODO(rado): make this configurable/smarter.
var VIEW_POOL_CAPACITY = 10000;
var VIEW_POOL_PREFILL = 0;
/**
* Const of making objects: http://jsperf.com/instantiate-size-of-object
*
* @publicModule angular2/template
*/
@IMPLEMENTS(ChangeDispatcher)
export class View {
/// This list matches the _nodes list. It is sparse, since only Elements have ElementInjector
rootElementInjectors:List<ElementInjector>;
elementInjectors:List<ElementInjector>;
bindElements:List;
textNodes:List;
changeDetector:ChangeDetector;
/// When the view is part of render tree, the DocumentFragment is empty, which is why we need
/// to keep track of the nodes.
nodes:List;
componentChildViews: List<View>;
viewContainers: List<ViewContainer>;
contentTags: List<Content>;
preBuiltObjects: List<PreBuiltObjects>;
lightDoms: List<LightDom>;
proto: ProtoView;
context: any;
locals:Locals;
constructor(proto:ProtoView, nodes:List, protoLocals:Map) {
this.proto = proto;
this.nodes = nodes;
this.changeDetector = null;
this.elementInjectors = null;
this.rootElementInjectors = null;
this.textNodes = null;
this.bindElements = null;
this.componentChildViews = null;
this.viewContainers = null;
this.contentTags = null;
this.preBuiltObjects = null;
this.lightDoms = null;
this.context = null;
this.locals = new Locals(null, MapWrapper.clone(protoLocals)); //TODO optimize this
}
init(changeDetector:ChangeDetector, elementInjectors:List, rootElementInjectors:List, textNodes: List, bindElements:List,
viewContainers:List, contentTags:List, preBuiltObjects:List, componentChildViews:List, lightDoms:List<LightDom>) {
this.changeDetector = changeDetector;
this.elementInjectors = elementInjectors;
this.rootElementInjectors = rootElementInjectors;
this.textNodes = textNodes;
this.bindElements = bindElements;
this.viewContainers = viewContainers;
this.contentTags = contentTags;
this.preBuiltObjects = preBuiltObjects;
this.componentChildViews = componentChildViews;
this.lightDoms = lightDoms;
}
setLocal(contextName: string, value) {
if (!this.hydrated()) throw new BaseException('Cannot set locals on dehydrated view.');
if (!MapWrapper.contains(this.proto.variableBindings, contextName)) {
return;
}
var templateName = MapWrapper.get(this.proto.variableBindings, contextName);
this.locals.set(templateName, value);
}
hydrated() {
return isPresent(this.context);
}
_hydrateContext(newContext, locals) {
this.context = newContext;
this.locals.parent = locals;
this.changeDetector.hydrate(this.context, this.locals);
}
_dehydrateContext() {
if (isPresent(this.locals)) {
this.locals.clearValues();
}
this.context = null;
this.changeDetector.dehydrate();
}
/**
* A dehydrated view is a state of the view that allows it to be moved around
* the view tree, without incurring the cost of recreating the underlying
* injectors and watch records.
*
* A dehydrated view has the following properties:
*
* - all element injectors are empty.
* - all appInjectors are released.
* - all viewcontainers are empty.
* - all context locals are set to null.
* - the view context is null.
*
* A call to hydrate/dehydrate does not attach/detach the view from the view
* tree.
*/
hydrate(appInjector: Injector, hostElementInjector: ElementInjector, hostLightDom: LightDom,
context: Object, locals:Locals) {
if (this.hydrated()) throw new BaseException('The view is already hydrated.');
this._hydrateContext(context, locals);
// viewContainers
for (var i = 0; i < this.viewContainers.length; i++) {
var vc = this.viewContainers[i];
if (isPresent(vc)) {
vc.hydrate(appInjector, hostElementInjector, hostLightDom);
}
}
var binders = this.proto.elementBinders;
var componentChildViewIndex = 0;
for (var i = 0; i < binders.length; ++i) {
var componentDirective = binders[i].componentDirective;
var shadowDomAppInjector = null;
// shadowDomAppInjector
if (isPresent(componentDirective)) {
var services = componentDirective.annotation.services;
if (isPresent(services))
shadowDomAppInjector = appInjector.createChild(services);
else {
shadowDomAppInjector = appInjector;
}
} else {
shadowDomAppInjector = null;
}
// elementInjectors
var elementInjector = this.elementInjectors[i];
if (isPresent(elementInjector)) {
elementInjector.instantiateDirectives(appInjector, shadowDomAppInjector, this.preBuiltObjects[i]);
// The exporting of $implicit is a special case. Since multiple elements will all export
// the different values as $implicit, directly assign $implicit bindings to the variable
// name.
var exportImplicitName = elementInjector.getExportImplicitName();
if (elementInjector.isExportingComponent()) {
this.locals.set(exportImplicitName, elementInjector.getComponent());
} else if (elementInjector.isExportingElement()) {
this.locals.set(exportImplicitName, elementInjector.getNgElement().domElement);
}
}
if (isPresent(binders[i].nestedProtoView) && isPresent(componentDirective)) {
this.componentChildViews[componentChildViewIndex++].hydrate(shadowDomAppInjector,
elementInjector, this.lightDoms[i], elementInjector.getComponent(), null);
}
}
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();
}
// elementInjectors
for (var i = 0; i < this.elementInjectors.length; i++) {
if (isPresent(this.elementInjectors[i])) {
this.elementInjectors[i].clearDirectives();
}
}
// viewContainers
if (isPresent(this.viewContainers)) {
for (var i = 0; i < this.viewContainers.length; i++) {
var vc = this.viewContainers[i];
if (isPresent(vc)) {
vc.dehydrate();
}
}
}
this._dehydrateContext();
}
/**
* Triggers the event handlers for the element and the directives.
*
* This method is intended to be called from directive EventEmitters.
*
* @param {string} eventName
* @param {*} eventObj
* @param {int} binderIndex
*/
triggerEventHandlers(eventName: string, eventObj, binderIndex: int) {
var handlers = this.proto.eventHandlers[binderIndex];
if (isBlank(handlers)) return;
var handler = StringMapWrapper.get(handlers, eventName);
if (isBlank(handler)) return;
handler(eventObj, this);
}
onAllChangesDone(directiveMemento:DirectiveMemento) {
var dir = directiveMemento.directive(this.elementInjectors);
dir.onAllChangesDone();
}
onChange(directiveMemento:DirectiveMemento, changes) {
var dir = directiveMemento.directive(this.elementInjectors);
dir.onChange(changes);
}
// dispatch to element injector or text nodes based on context
invokeMementoFor(memento:any, currentValue:any) {
if (memento instanceof DirectiveBindingMemento) {
var directiveMemento:DirectiveBindingMemento = memento;
directiveMemento.invoke(currentValue, this.elementInjectors);
} else if (memento instanceof ElementBindingMemento) {
var elementMemento:ElementBindingMemento = memento;
elementMemento.invoke(currentValue, this.bindElements);
} else {
// we know it refers to _textNodes.
var textNodeIndex:number = memento;
DOM.setText(this.textNodes[textNodeIndex], currentValue);
}
}
}
/**
*
* @publicModule angular2/template
*/
export class ProtoView {
element;
elementBinders:List<ElementBinder>;
protoChangeDetector:ProtoChangeDetector;
variableBindings: Map;
protoLocals:Map;
textNodesWithBindingCount:int;
elementsWithBindingCount:int;
instantiateInPlace:boolean;
rootBindingOffset:int;
isTemplateElement:boolean;
shadowDomStrategy: ShadowDomStrategy;
_viewPool: ViewPool;
stylePromises: List<Promise>;
// List<Map<eventName, handler>>, indexed by binder index
eventHandlers:List;
bindingRecords:List;
parentProtoView:ProtoView;
_variableBindings:List;
_directiveMementosMap:Map;
_directiveMementos:List;
render:renderApi.ProtoViewRef;
constructor(
render:renderApi.ProtoViewRef,
template,
protoChangeDetector:ProtoChangeDetector,
shadowDomStrategy:ShadowDomStrategy, parentProtoView:ProtoView = null) {
this.render = render;
this.element = template;
this.elementBinders = [];
this.variableBindings = MapWrapper.create();
this.protoLocals = MapWrapper.create();
this.protoChangeDetector = protoChangeDetector;
this.parentProtoView = parentProtoView;
this.textNodesWithBindingCount = 0;
this.elementsWithBindingCount = 0;
this.instantiateInPlace = false;
this.rootBindingOffset = (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS))
? 1 : 0;
this.isTemplateElement = DOM.isTemplateElement(this.element);
this.shadowDomStrategy = shadowDomStrategy;
this._viewPool = new ViewPool(VIEW_POOL_CAPACITY);
this.stylePromises = [];
this.eventHandlers = [];
this.bindingRecords = [];
this._directiveMementosMap = MapWrapper.create();
this._variableBindings = null;
this._directiveMementos = null;
}
// TODO(rado): hostElementInjector should be moved to hydrate phase.
instantiate(hostElementInjector: ElementInjector, eventManager: EventManager):View {
if (this._viewPool.length() == 0) this._preFillPool(hostElementInjector, eventManager);
var view = this._viewPool.pop();
return isPresent(view) ? view : this._instantiate(hostElementInjector, eventManager);
}
_preFillPool(hostElementInjector: ElementInjector, eventManager: EventManager) {
for (var i = 0; i < VIEW_POOL_PREFILL; i++) {
this._viewPool.push(this._instantiate(hostElementInjector, eventManager));
}
}
//TODO: Tobias or Victor. Moving it into the constructor.
// this work should be done the constructor of ProtoView once we separate
// ProtoView and ProtoViewBuilder
_getVariableBindings() {
if (isPresent(this._variableBindings)) {
return this._variableBindings;
}
this._variableBindings = isPresent(this.parentProtoView) ?
ListWrapper.clone(this.parentProtoView._getVariableBindings()) : [];
MapWrapper.forEach(this.protoLocals, (v, local) => {
ListWrapper.push(this._variableBindings, local);
});
return this._variableBindings;
}
//TODO: Tobias or Victor. Moving it into the constructor.
// this work should be done the constructor of ProtoView once we separate
// ProtoView and ProtoViewBuilder
_getDirectiveMementos() {
if (isPresent(this._directiveMementos)) {
return this._directiveMementos;
}
this._directiveMementos = [];
for (var injectorIndex = 0; injectorIndex < this.elementBinders.length; ++injectorIndex) {
var pei = this.elementBinders[injectorIndex].protoElementInjector;
if (isPresent(pei)) {
for (var directiveIndex = 0; directiveIndex < pei.numberOfDirectives; ++directiveIndex) {
ListWrapper.push(this._directiveMementos, this._getDirectiveMemento(injectorIndex, directiveIndex));
}
}
}
return this._directiveMementos;
}
_instantiate(hostElementInjector: ElementInjector, eventManager: EventManager): View {
var rootElementClone = this.instantiateInPlace ? this.element : DOM.importIntoDoc(this.element);
var elementsWithBindingsDynamic;
if (this.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 viewNodes;
if (this.isTemplateElement) {
var childNode = DOM.firstChild(DOM.content(rootElementClone));
viewNodes = []; // 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(viewNodes, childNode);
childNode = DOM.nextSibling(childNode);
}
} else {
viewNodes = [rootElementClone];
}
var view = new View(this, viewNodes, this.protoLocals);
var changeDetector = this.protoChangeDetector.instantiate(view, this.bindingRecords,
this._getVariableBindings(), this._getDirectiveMementos());
var binders = this.elementBinders;
var elementInjectors = ListWrapper.createFixedSize(binders.length);
var eventHandlers = ListWrapper.createFixedSize(binders.length);
var rootElementInjectors = [];
var textNodes = [];
var elementsWithPropertyBindings = [];
var preBuiltObjects = ListWrapper.createFixedSize(binders.length);
var viewContainers = ListWrapper.createFixedSize(binders.length);
var contentTags = ListWrapper.createFixedSize(binders.length);
var componentChildViews = [];
var lightDoms = ListWrapper.createFixedSize(binders.length);
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
var element;
if (binderIdx === 0 && this.rootBindingOffset === 1) {
element = rootElementClone;
} else {
element = elementsWithBindings[binderIdx - this.rootBindingOffset];
}
var elementInjector = null;
// elementInjectors and rootElementInjectors
var protoElementInjector = binder.protoElementInjector;
if (isPresent(protoElementInjector)) {
if (isPresent(protoElementInjector.parent)) {
var parentElementInjector = elementInjectors[protoElementInjector.parent.index];
elementInjector = protoElementInjector.instantiate(parentElementInjector, null);
} else {
elementInjector = protoElementInjector.instantiate(null, hostElementInjector);
ListWrapper.push(rootElementInjectors, elementInjector);
}
}
elementInjectors[binderIdx] = elementInjector;
if (binder.hasElementPropertyBindings) {
ListWrapper.push(elementsWithPropertyBindings, element);
}
// textNodes
var textNodeIndices = binder.textNodeIndices;
if (isPresent(textNodeIndices)) {
var childNode = DOM.firstChild(DOM.templateAwareRoot(element));
for (var j = 0, k = 0; j < textNodeIndices.length; j++) {
for(var index = textNodeIndices[j]; k < index; k++) {
childNode = DOM.nextSibling(childNode);
}
ListWrapper.push(textNodes, childNode);
}
}
// componentChildViews
var lightDom = null;
var bindingPropagationConfig = null;
if (isPresent(binder.nestedProtoView) && isPresent(binder.componentDirective)) {
var strategy = this.shadowDomStrategy;
var childView = binder.nestedProtoView.instantiate(elementInjector, eventManager);
changeDetector.addChild(childView.changeDetector);
lightDom = strategy.constructLightDom(view, childView, element);
strategy.attachTemplate(element, childView);
bindingPropagationConfig = new BindingPropagationConfig(childView.changeDetector);
ListWrapper.push(componentChildViews, childView);
}
lightDoms[binderIdx] = lightDom;
var destLightDom = null;
if (isPresent(binder.parent) && binder.distanceToParent === 1) {
destLightDom = lightDoms[binder.parent.index];
}
// viewContainers
var viewContainer = null;
if (isPresent(binder.viewportDirective)) {
viewContainer = new ViewContainer(view, element, binder.nestedProtoView, elementInjector,
eventManager, destLightDom);
}
viewContainers[binderIdx] = viewContainer;
// contentTags
var contentTag = null;
if (isPresent(binder.contentTagSelector)) {
contentTag = new Content(destLightDom, element, binder.contentTagSelector);
}
contentTags[binderIdx] = contentTag;
// preBuiltObjects
if (isPresent(elementInjector)) {
preBuiltObjects[binderIdx] = new PreBuiltObjects(view, new NgElement(element), viewContainer,
bindingPropagationConfig);
}
// events
if (isPresent(binder.events)) {
eventHandlers[binderIdx] = StringMapWrapper.create();
StringMapWrapper.forEach(binder.events, (eventMap, eventName) => {
var handler = ProtoView.buildEventHandler(eventMap, binderIdx);
StringMapWrapper.set(eventHandlers[binderIdx], eventName, handler);
if (isBlank(elementInjector) || !elementInjector.hasEventEmitter(eventName)) {
eventManager.addEventListener(element, eventName,
(event) => { handler(event, view); });
}
});
}
}
this.eventHandlers = eventHandlers;
view.init(changeDetector, elementInjectors, rootElementInjectors, textNodes, elementsWithPropertyBindings,
viewContainers, contentTags, preBuiltObjects, componentChildViews, lightDoms);
return view;
}
returnToPool(view: View) {
this._viewPool.push(view);
}
/**
* Creates an event handler.
*
* @param {Map} eventMap Map directiveIndexes to expressions
* @param {int} injectorIdx
* @returns {Function}
*/
static buildEventHandler(eventMap: Map, injectorIdx: int) {
var locals = MapWrapper.create();
return (event, view) => {
// Most of the time the event will be fired only when the view is in the live document.
// However, in a rare circumstance the view might get dehydrated, in between the event
// queuing up and firing.
if (view.hydrated()) {
MapWrapper.set(locals, '$event', event);
MapWrapper.forEach(eventMap, (expr, directiveIndex) => {
var context;
if (directiveIndex === -1) {
context = view.context;
} else {
context = view.elementInjectors[injectorIdx].getDirectiveAtIndex(directiveIndex);
}
expr.eval(context, new Locals(view.locals, locals));
});
}
}
}
bindVariable(contextName:string, templateName:string) {
MapWrapper.set(this.variableBindings, contextName, templateName);
MapWrapper.set(this.protoLocals, templateName, null);
}
bindElement(parent:ElementBinder, distanceToParent:int, protoElementInjector:ProtoElementInjector,
componentDirective:DirectiveBinding = null, viewportDirective:DirectiveBinding = null):ElementBinder {
var elBinder = new ElementBinder(this.elementBinders.length, parent, distanceToParent,
protoElementInjector, componentDirective, viewportDirective);
ListWrapper.push(this.elementBinders, elBinder);
return elBinder;
}
/**
* Adds a text node binding for the last created ElementBinder via bindElement
*/
bindTextNode(indexInParent:int, expression:AST) {
var elBinder = this.elementBinders[this.elementBinders.length-1];
if (isBlank(elBinder.textNodeIndices)) {
elBinder.textNodeIndices = ListWrapper.create();
}
ListWrapper.push(elBinder.textNodeIndices, indexInParent);
var memento = this.textNodesWithBindingCount++;
ListWrapper.push(this.bindingRecords, new BindingRecord(expression, memento, null));
}
/**
* Adds an element property binding for the last created ElementBinder via bindElement
*/
bindElementProperty(expression:AST, setterName:string, setter:SetterFn) {
var elBinder = this.elementBinders[this.elementBinders.length-1];
if (!elBinder.hasElementPropertyBindings) {
elBinder.hasElementPropertyBindings = true;
this.elementsWithBindingCount++;
}
var memento = new ElementBindingMemento(this.elementsWithBindingCount-1, setter);
ListWrapper.push(this.bindingRecords, new BindingRecord(expression, memento, null));
}
/**
* Adds an event binding for the last created ElementBinder via bindElement.
*
* If the directive index is a positive integer, the event is evaluated in the context of
* the given directive.
*
* If the directive index is -1, the event is evaluated in the context of the enclosing view.
*
* @param {string} eventName
* @param {AST} expression
* @param {int} directiveIndex The directive index in the binder or -1 when the event is not bound
* to a directive
*/
bindEvent(eventName:string, expression:AST, directiveIndex: int = -1) {
var elBinder = this.elementBinders[this.elementBinders.length - 1];
var events = elBinder.events;
if (isBlank(events)) {
events = StringMapWrapper.create();
elBinder.events = events;
}
var event = StringMapWrapper.get(events, eventName);
if (isBlank(event)) {
event = MapWrapper.create();
StringMapWrapper.set(events, eventName, event);
}
MapWrapper.set(event, directiveIndex, expression);
}
/**
* Adds a directive property binding for the last created ElementBinder via bindElement
*/
bindDirectiveProperty(
directiveIndex:number,
expression:AST,
setterName:string,
setter:SetterFn) {
var elementIndex = this.elementBinders.length-1;
var bindingMemento = new DirectiveBindingMemento(
elementIndex,
directiveIndex,
setterName,
setter
);
var directiveMemento = this._getDirectiveMemento(elementIndex, directiveIndex);
ListWrapper.push(this.bindingRecords, new BindingRecord(expression, bindingMemento, directiveMemento));
}
_getDirectiveMemento(elementInjectorIndex:number, directiveIndex:number) {
var id = elementInjectorIndex * 100 + directiveIndex;
var protoElementInjector = this.elementBinders[elementInjectorIndex].protoElementInjector;
if (!MapWrapper.contains(this._directiveMementosMap, id)) {
var binding = protoElementInjector.getDirectiveBindingAtIndex(directiveIndex);
MapWrapper.set(this._directiveMementosMap, id,
new DirectiveMemento(elementInjectorIndex, directiveIndex,
binding.callOnAllChangesDone, binding.callOnChange));
}
return MapWrapper.get(this._directiveMementosMap, id);
}
}
/**
*/
export class ElementBindingMemento {
_elementIndex:int;
_setter:SetterFn;
constructor(elementIndex:int, setter:SetterFn) {
this._elementIndex = elementIndex;
this._setter = setter;
}
invoke(currentValue:any, bindElements:List) {
var element = bindElements[this._elementIndex];
this._setter(element, currentValue);
}
}
/**
*/
export class DirectiveBindingMemento {
_elementInjectorIndex:int;
_directiveIndex:int;
propertyName:string;
_setter:SetterFn;
constructor(
elementInjectorIndex:number,
directiveIndex:number,
propertyName:string,
setter:SetterFn) {
this._elementInjectorIndex = elementInjectorIndex;
this._directiveIndex = directiveIndex;
this.propertyName = propertyName;
this._setter = setter;
}
invoke(currentValue:any, elementInjectors:List<ElementInjector>) {
var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex];
var directive = elementInjector.getDirectiveAtIndex(this._directiveIndex);
this._setter(directive, currentValue);
}
}
class DirectiveMemento {
_elementInjectorIndex:number;
_directiveIndex:number;
callOnAllChangesDone:boolean;
callOnChange:boolean;
constructor(elementInjectorIndex:number, directiveIndex:number, callOnAllChangesDone:boolean,
callOnChange:boolean) {
this._elementInjectorIndex = elementInjectorIndex;
this._directiveIndex = directiveIndex;
this.callOnAllChangesDone = callOnAllChangesDone;
this.callOnChange = callOnChange;
}
directive(elementInjectors:List<ElementInjector>) {
var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex];
return elementInjector.getDirectiveAtIndex(this._directiveIndex);
}
}