526 lines
18 KiB
JavaScript
Raw Normal View History

2014-10-10 20:44:55 -07:00
import {DOM, Element, Node, Text, DocumentFragment, TemplateElement} from 'facade/dom';
import {ListWrapper, MapWrapper, StringMapWrapper, List} from 'facade/collection';
import {ProtoRecordRange, RecordRange, ChangeDispatcher} from 'change_detection/record_range';
2014-09-28 16:29:11 -07:00
import {Record} from 'change_detection/record';
import {AST} from 'change_detection/parser/ast';
2014-11-14 10:58:58 -08:00
import {ProtoElementInjector, ElementInjector, PreBuiltObjects} from './element_injector';
import {ElementBinder} from './element_binder';
import {DirectiveMetadata} from './directive_metadata';
import {SetterFn} from 'reflection/types';
import {FIELD, IMPLEMENTS, int, isPresent, isBlank, BaseException} from 'facade/lang';
import {Injector} from 'di/di';
2014-11-14 10:58:58 -08:00
import {NgElement} from 'core/dom/element';
import {ViewPort} from './viewport';
import {OnChange} from './interfaces';
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
2014-09-28 16:29:11 -07:00
const NG_BINDING_CLASS = 'ng-binding';
const NG_BINDING_CLASS_SELECTOR = '.ng-binding';
// TODO(tbosch): Cannot use `const` because of Dart.
var NO_FORMATTERS = MapWrapper.create();
/**
2014-10-10 20:44:55 -07:00
* Const of making objects: http://jsperf.com/instantiate-size-of-object
*/
@IMPLEMENTS(ChangeDispatcher)
2014-09-28 16:29:11 -07:00
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<Element>;
textNodes:List<Text>;
recordRange:RecordRange;
2014-09-28 16:29:11 -07:00
/// 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<Node>;
componentChildViews: List<View>;
viewPorts: List<ViewPort>;
preBuiltObjects: List<PreBuiltObjects>;
proto: ProtoView;
context: Object;
contextWithLocals:ContextWithVariableBindings;
constructor(proto:ProtoView, nodes:List<Node>, protoRecordRange:ProtoRecordRange, protoContextLocals:Map) {
this.proto = proto;
this.nodes = nodes;
this.recordRange = protoRecordRange.instantiate(this, NO_FORMATTERS);
this.elementInjectors = null;
this.rootElementInjectors = null;
this.textNodes = null;
this.bindElements = null;
this.componentChildViews = null;
this.viewPorts = null;
this.preBuiltObjects = null;
this.context = null;
// contextWithLocals
if (MapWrapper.size(protoContextLocals) > 0) {
this.contextWithLocals = new ContextWithVariableBindings(null, MapWrapper.clone(protoContextLocals));
} else {
this.contextWithLocals = null;
}
}
init(elementInjectors:List, rootElementInjectors:List, textNodes: List, bindElements:List, viewPorts:List, preBuiltObjects:List, componentChildViews:List) {
this.elementInjectors = elementInjectors;
this.rootElementInjectors = rootElementInjectors;
this.textNodes = textNodes;
this.bindElements = bindElements;
this.viewPorts = viewPorts;
this.preBuiltObjects = preBuiltObjects;
this.componentChildViews = componentChildViews;
}
setLocal(contextName: string, value) {
if (!this.hydrated()) throw new BaseException('Cannot set locals on dehydrated view.');
if (!MapWrapper.contains(this.proto.variableBindings, contextName)) {
throw new BaseException(
`Local binding ${contextName} not defined in the view template.`);
}
var templateName = MapWrapper.get(this.proto.variableBindings, contextName);
this.context.set(templateName, value);
}
hydrated() {
return isPresent(this.context);
}
_hydrateContext(newContext) {
if (isPresent(this.contextWithLocals)) {
this.contextWithLocals.parent = newContext;
this.context = this.contextWithLocals;
} else {
this.context = newContext;
}
// TODO(tbosch): if we have a contextWithLocals we actually only need to
// set the contextWithLocals once. Would it be faster to always use a contextWithLocals
// even if we don't have locals and not update the recordRange here?
this.recordRange.setContext(this.context);
}
_dehydrateContext() {
if (isPresent(this.contextWithLocals)) {
this.contextWithLocals.clearValues();
}
this.context = null;
}
/**
* 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 viewports 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,
context: Object) {
if (this.hydrated()) throw new BaseException('The view is already hydrated.');
this._hydrateContext(context);
// viewPorts
for (var i = 0; i < this.viewPorts.length; i++) {
this.viewPorts[i].hydrate(appInjector, hostElementInjector);
}
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.componentServices;
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]);
}
// componentChildViews
if (isPresent(shadowDomAppInjector)) {
this.componentChildViews[componentChildViewIndex++].hydrate(shadowDomAppInjector,
elementInjector, elementInjector.getComponent());
}
}
}
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++) {
this.elementInjectors[i].clearDirectives();
}
// viewPorts
if (isPresent(this.viewPorts)) {
for (var i = 0; i < this.viewPorts.length; i++) {
this.viewPorts[i].dehydrate();
}
}
this._dehydrateContext();
2014-09-28 16:29:11 -07:00
}
onRecordChange(groupMemento, records:List<Record>) {
this._invokeMementoForRecords(records);
if (groupMemento instanceof DirectivePropertyGroupMemento) {
this._notifyDirectiveAboutChanges(groupMemento, records);
}
}
_invokeMementoForRecords(records:List<Record>) {
for(var i = 0; i < records.length; ++i) {
this._invokeMementoFor(records[i]);
}
}
_notifyDirectiveAboutChanges(groupMemento, records:List<Record>) {
var dir = groupMemento.directive(this.elementInjectors);
if (dir instanceof OnChange) {
dir.onChange(this._collectChanges(records));
}
}
2014-09-28 16:29:11 -07:00
// dispatch to element injector or text nodes based on context
_invokeMementoFor(record:Record) {
var memento = record.expressionMemento();
if (memento instanceof DirectivePropertyMemento) {
2014-10-10 20:44:55 -07:00
// we know that it is DirectivePropertyMemento
var directiveMemento:DirectivePropertyMemento = memento;
2014-10-10 20:44:55 -07:00
directiveMemento.invoke(record, this.elementInjectors);
} else if (memento instanceof ElementPropertyMemento) {
var elementMemento:ElementPropertyMemento = memento;
2014-10-10 20:44:55 -07:00
elementMemento.invoke(record, this.bindElements);
2014-09-28 16:29:11 -07:00
} else {
2014-10-10 20:44:55 -07:00
// we know it refers to _textNodes.
var textNodeIndex:number = memento;
2014-10-10 20:44:55 -07:00
DOM.setText(this.textNodes[textNodeIndex], record.currentValue);
2014-09-28 16:29:11 -07:00
}
}
2014-12-04 13:23:32 +01:00
_collectChanges(records:List<Record>) {
var changes = StringMapWrapper.create();
for(var i = 0; i < records.length; ++i) {
var record = records[i];
var propertyUpdate = new PropertyUpdate(record.currentValue, record.previousValue);
StringMapWrapper.set(changes, record.expressionMemento()._setterName, propertyUpdate);
}
return changes;
}
2014-09-28 16:29:11 -07:00
}
2014-09-28 20:02:32 -07:00
export class ProtoView {
element:Element;
elementBinders:List<ElementBinder>;
protoRecordRange:ProtoRecordRange;
variableBindings: Map;
protoContextLocals:Map;
textNodesWithBindingCount:int;
elementsWithBindingCount:int;
instantiateInPlace:boolean;
rootBindingOffset:int;
isTemplateElement:boolean;
2014-09-28 20:02:32 -07:00
constructor(
template:Element,
protoRecordRange:ProtoRecordRange) {
this.element = template;
this.elementBinders = [];
this.variableBindings = MapWrapper.create();
this.protoContextLocals = MapWrapper.create();
this.protoRecordRange = protoRecordRange;
this.textNodesWithBindingCount = 0;
this.elementsWithBindingCount = 0;
this.instantiateInPlace = false;
if (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) {
this.rootBindingOffset = 1;
} else {
this.rootBindingOffset = 0;
}
this.isTemplateElement = this.element instanceof TemplateElement;
2014-09-28 20:02:32 -07:00
}
// TODO(rado): hostElementInjector should be moved to hydrate phase.
instantiate(hostElementInjector: ElementInjector):View {
var rootElementClone = this.instantiateInPlace ? this.element : DOM.clone(this.element);
var elementsWithBindings;
if (this.isTemplateElement) {
elementsWithBindings = DOM.querySelectorAll(rootElementClone.content, NG_BINDING_CLASS_SELECTOR);
} else {
elementsWithBindings = DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS);
}
var viewNodes;
if (this.isTemplateElement) {
var childNodes = rootElementClone.content.childNodes;
// Note: An explicit loop is the fastes way to convert a DOM array into a JS array!
viewNodes = ListWrapper.createFixedSize(childNodes.length);
for (var i=0; i<childNodes.length; i++) {
viewNodes[i] = childNodes[i];
}
} else {
viewNodes = [rootElementClone];
}
var view = new View(this, viewNodes, this.protoRecordRange, this.protoContextLocals);
var binders = this.elementBinders;
var elementInjectors = ListWrapper.createFixedSize(binders.length);
var rootElementInjectors = [];
var textNodes = [];
var elementsWithPropertyBindings = [];
var preBuiltObjects = ListWrapper.createFixedSize(binders.length);
var viewPorts = [];
var componentChildViews = [];
for (var i = 0; i < binders.length; i++) {
var binder = binders[i];
var element;
if (i === 0 && this.rootBindingOffset === 1) {
element = rootElementClone;
} else {
element = elementsWithBindings[i - this.rootBindingOffset];
}
var elementInjector = null;
// elementInjectors and rootElementInjectors
var protoElementInjector = binder.protoElementInjector;
if (isPresent(protoElementInjector)) {
var parentElementInjector = isPresent(protoElementInjector.parent) ? elementInjectors[protoElementInjector.parent.index] : null;
elementInjector = protoElementInjector.instantiate(parentElementInjector, hostElementInjector);
if (isBlank(parentElementInjector)) {
ListWrapper.push(rootElementInjectors, elementInjector);
}
}
elementInjectors[i] = elementInjector;
// viewPorts
var viewPort = null;
if (isPresent(binder.templateDirective)) {
viewPort = new ViewPort(view, element, binder.nestedProtoView, elementInjector);
ListWrapper.push(viewPorts, viewPort);
}
// preBuiltObjects
var preBuiltObject = null;
if (isPresent(elementInjector)) {
preBuiltObject = new PreBuiltObjects(view, new NgElement(element), viewPort);
}
preBuiltObjects[i] = preBuiltObject;
// elementsWithPropertyBindings
if (binder.hasElementPropertyBindings) {
ListWrapper.push(elementsWithPropertyBindings, element);
}
2014-11-14 10:58:58 -08:00
// textNodes
var textNodeIndices = binder.textNodeIndices;
if (isPresent(textNodeIndices)) {
var childNodes = DOM.templateAwareRoot(element).childNodes;
for (var j = 0; j < textNodeIndices.length; j++) {
ListWrapper.push(textNodes, childNodes[textNodeIndices[j]]);
}
}
// componentChildViews
if (isPresent(binder.componentDirective)) {
var childView = binder.nestedProtoView.instantiate(elementInjector);
view.recordRange.addRange(childView.recordRange);
binder.componentDirective.shadowDomStrategy.attachTemplate(element, childView);
ListWrapper.push(componentChildViews, childView);
}
}
view.init(elementInjectors, rootElementInjectors, textNodes, elementsWithPropertyBindings,
viewPorts, preBuiltObjects, componentChildViews);
2014-11-14 10:58:58 -08:00
return view;
}
bindVariable(contextName:string, templateName:string) {
MapWrapper.set(this.variableBindings, contextName, templateName);
MapWrapper.set(this.protoContextLocals, templateName, null);
}
bindElement(protoElementInjector:ProtoElementInjector,
componentDirective:DirectiveMetadata = null, templateDirective:DirectiveMetadata = null):ElementBinder {
var elBinder = new ElementBinder(protoElementInjector, componentDirective, templateDirective);
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];
2014-11-19 14:54:07 -08:00
if (isBlank(elBinder.textNodeIndices)) {
elBinder.textNodeIndices = ListWrapper.create();
}
ListWrapper.push(elBinder.textNodeIndices, indexInParent);
var memento = this.textNodesWithBindingCount++;
this.protoRecordRange.addRecordsFromAST(expression, memento, memento);
}
/**
* 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 ElementPropertyMemento(this.elementsWithBindingCount-1, setterName, setter);
this.protoRecordRange.addRecordsFromAST(expression, memento, memento);
}
2014-11-19 14:54:07 -08:00
/**
* Adds an event binding for the last created ElementBinder via bindElement
*/
bindEvent(eventName:string, expression:AST) {
var elBinder = this.elementBinders[this.elementBinders.length-1];
if (isBlank(elBinder.events)) {
elBinder.events = MapWrapper.create();
}
MapWrapper.set(elBinder.events, eventName, expression);
}
/**
* Adds a directive property binding for the last created ElementBinder via bindElement
*/
bindDirectiveProperty(
directiveIndex:number,
expression:AST,
setterName:string,
setter:SetterFn,
isContentWatch: boolean) {
var expMemento = new DirectivePropertyMemento(
this.elementBinders.length-1,
directiveIndex,
setterName,
setter
);
var groupMemento = DirectivePropertyGroupMemento.get(expMemento);
this.protoRecordRange.addRecordsFromAST(expression, expMemento, groupMemento, isContentWatch);
}
// Create a rootView as if the compiler encountered <rootcmp></rootcmp>,
// and the component template is already compiled into protoView.
// Used for bootstrapping.
static createRootProtoView(protoView: ProtoView,
insertionElement, rootComponentAnnotatedType: DirectiveMetadata): ProtoView {
DOM.addClass(insertionElement, 'ng-binding');
var rootProtoView = new ProtoView(insertionElement, new ProtoRecordRange());
rootProtoView.instantiateInPlace = true;
var binder = rootProtoView.bindElement(
new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true));
binder.componentDirective = rootComponentAnnotatedType;
binder.nestedProtoView = protoView;
return rootProtoView;
}
2014-09-28 20:02:32 -07:00
}
2014-10-10 20:44:55 -07:00
export class ElementPropertyMemento {
_elementIndex:int;
_setterName:string;
_setter:SetterFn;
constructor(elementIndex:int, setterName:string, setter:SetterFn) {
2014-10-10 20:44:55 -07:00
this._elementIndex = elementIndex;
this._setterName = setterName;
this._setter = setter;
2014-10-10 20:44:55 -07:00
}
invoke(record:Record, bindElements:List<Element>) {
var element:Element = bindElements[this._elementIndex];
this._setter(element, record.currentValue);
2014-10-10 20:44:55 -07:00
}
}
2014-09-28 16:29:11 -07:00
2014-10-10 20:44:55 -07:00
export class DirectivePropertyMemento {
_elementInjectorIndex:int;
_directiveIndex:int;
_setterName:string;
_setter:SetterFn;
2014-09-28 16:29:11 -07:00
constructor(
elementInjectorIndex:number,
directiveIndex:number,
setterName:string,
setter:SetterFn) {
2014-09-28 16:29:11 -07:00
this._elementInjectorIndex = elementInjectorIndex;
this._directiveIndex = directiveIndex;
this._setterName = setterName;
this._setter = setter;
}
invoke(record:Record, elementInjectors:List<ElementInjector>) {
var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex];
var directive = elementInjector.getAtIndex(this._directiveIndex);
2014-09-28 16:29:11 -07:00
this._setter(directive, record.currentValue);
}
}
var _groups = MapWrapper.create();
class DirectivePropertyGroupMemento {
_elementInjectorIndex:number;
_directiveIndex:number;
constructor(elementInjectorIndex:number, directiveIndex:number) {
this._elementInjectorIndex = elementInjectorIndex;
this._directiveIndex = directiveIndex;
}
static get(memento:DirectivePropertyMemento) {
var elementInjectorIndex = memento._elementInjectorIndex;
var directiveIndex = memento._directiveIndex;
var id = elementInjectorIndex * 100 + directiveIndex;
if (!MapWrapper.contains(_groups, id)) {
MapWrapper.set(_groups, id, new DirectivePropertyGroupMemento(elementInjectorIndex, directiveIndex));
}
return MapWrapper.get(_groups, id);
}
directive(elementInjectors:List<ElementInjector>) {
var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex];
return elementInjector.getAtIndex(this._directiveIndex);
}
}
class PropertyUpdate {
currentValue;
previousValue;
constructor(currentValue, previousValue) {
this.currentValue = currentValue;
this.previousValue = previousValue;
}
}