361 lines
13 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, List} from 'facade/collection';
import {ProtoRecordRange, RecordRange, WatchGroupDispatcher} 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 {AnnotatedType} from './annotated_type';
import {SetterFn} from 'reflection/types';
import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang';
import {Injector} from 'di/di';
2014-11-14 10:58:58 -08:00
import {NgElement} from 'core/dom/element';
2014-09-28 16:29:11 -07:00
const NG_BINDING_CLASS = 'ng-binding';
2014-10-10 20:44:55 -07:00
/***
* Const of making objects: http://jsperf.com/instantiate-size-of-object
*/
2014-09-28 16:29:11 -07:00
@IMPLEMENTS(WatchGroupDispatcher)
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>;
onChangeDispatcher:OnChangeDispatcher;
childViews: List<View>;
constructor(nodes:List<Node>, elementInjectors:List,
rootElementInjectors:List, textNodes:List, bindElements:List,
protoRecordRange:ProtoRecordRange, context) {
this.nodes = nodes;
this.elementInjectors = elementInjectors;
this.rootElementInjectors = rootElementInjectors;
2014-10-10 20:44:55 -07:00
this.onChangeDispatcher = null;
this.textNodes = textNodes;
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 = [];
2014-09-28 16:29:11 -07:00
}
onRecordChange(record:Record, target) {
// dispatch to element injector or text nodes based on context
2014-10-10 20:44:55 -07:00
if (target instanceof DirectivePropertyMemento) {
// we know that it is DirectivePropertyMemento
var directiveMemento:DirectivePropertyMemento = target;
directiveMemento.invoke(record, this.elementInjectors);
} else if (target instanceof ElementPropertyMemento) {
2014-10-10 20:44:55 -07:00
var elementMemento:ElementPropertyMemento = target;
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.
2014-09-28 16:29:11 -07:00
var textNodeIndex:number = target;
2014-10-10 20:44:55 -07:00
DOM.setText(this.textNodes[textNodeIndex], record.currentValue);
2014-09-28 16:29:11 -07:00
}
}
addChild(childView: View) {
ListWrapper.push(this.childViews, childView);
this.recordRange.addRange(childView.recordRange);
}
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;
textNodesWithBindingCount:int;
elementsWithBindingCount:int;
2014-09-28 20:02:32 -07:00
constructor(
template:Element,
protoRecordRange:ProtoRecordRange) {
this.element = template;
this.elementBinders = [];
this.variableBindings = MapWrapper.create();
this.protoRecordRange = protoRecordRange;
this.textNodesWithBindingCount = 0;
this.elementsWithBindingCount = 0;
2014-09-28 20:02:32 -07:00
}
instantiate(context, lightDomAppInjector:Injector,
hostElementInjector: ElementInjector, inPlace:boolean = false):View {
var clone = inPlace ? this.element : DOM.clone(this.element);
var elements;
if (clone instanceof TemplateElement) {
elements = ListWrapper.clone(DOM.querySelectorAll(clone.content, `.${NG_BINDING_CLASS}`));
} else {
elements = ListWrapper.clone(DOM.getElementsByClassName(clone, NG_BINDING_CLASS));
}
if (DOM.hasClass(clone, NG_BINDING_CLASS)) {
ListWrapper.insert(elements, 0, clone);
}
var binders = this.elementBinders;
/**
* TODO: vsavkin: benchmark
* If this performs poorly, the seven loops can be collapsed into one.
*/
var elementInjectors = ProtoView._createElementInjectors(elements, binders, hostElementInjector);
var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors);
var textNodes = ProtoView._textNodes(elements, binders);
var bindElements = ProtoView._bindElements(elements, binders);
var shadowAppInjectors = ProtoView._createShadowAppInjectors(binders, lightDomAppInjector);
var viewNodes;
if (clone instanceof TemplateElement) {
viewNodes = ListWrapper.clone(clone.content.childNodes);
} else {
viewNodes = [clone];
}
2014-11-14 10:58:58 -08:00
var view = new View(viewNodes, elementInjectors, rootElementInjectors, textNodes,
bindElements, this.protoRecordRange, context);
2014-11-14 10:58:58 -08:00
ProtoView._instantiateDirectives(
view, elements, elementInjectors, lightDomAppInjector, shadowAppInjectors);
ProtoView._instantiateChildComponentViews(
elements, binders, elementInjectors, shadowAppInjectors, view);
2014-11-14 10:58:58 -08:00
return view;
}
bindVariable(contextName:string, templateName:string) {
MapWrapper.set(this.variableBindings, contextName, templateName);
}
bindElement(protoElementInjector:ProtoElementInjector,
componentDirective:AnnotatedType = null, templateDirective:AnnotatedType = 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);
this.protoRecordRange.addRecordsFromAST(expression, this.textNodesWithBindingCount++);
}
/**
* Adds an element property binding for the last created ElementBinder via bindElement
*/
bindElementProperty(propertyName:string, expression:AST) {
var elBinder = this.elementBinders[this.elementBinders.length-1];
if (!elBinder.hasElementPropertyBindings) {
elBinder.hasElementPropertyBindings = true;
this.elementsWithBindingCount++;
}
this.protoRecordRange.addRecordsFromAST(expression,
new ElementPropertyMemento(
this.elementsWithBindingCount-1,
propertyName
)
);
}
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) {
this.protoRecordRange.addRecordsFromAST(
expression,
new DirectivePropertyMemento(
this.elementBinders.length-1,
directiveIndex,
setterName,
setter
)
);
}
static _createElementInjectors(elements, binders, hostElementInjector) {
var injectors = ListWrapper.createFixedSize(binders.length);
for (var i = 0; i < binders.length; ++i) {
var proto = binders[i].protoElementInjector;
if (isPresent(proto)) {
var parentElementInjector = isPresent(proto.parent) ? injectors[proto.parent.index] : null;
injectors[i] = proto.instantiate(parentElementInjector, hostElementInjector);
} else {
injectors[i] = null;
}
}
return injectors;
}
static _instantiateDirectives(
view: View, elements:List, injectors:List<ElementInjectors>, lightDomAppInjector: Injector,
shadowDomAppInjectors:List<Injectors>) {
for (var i = 0; i < injectors.length; ++i) {
2014-11-14 10:58:58 -08:00
var preBuiltObjs = new PreBuiltObjects(view, new NgElement(elements[i]));
if (injectors[i] != null) injectors[i].instantiateDirectives(
lightDomAppInjector, shadowDomAppInjectors[i], preBuiltObjs);
}
}
static _rootElementInjectors(injectors) {
return ListWrapper.filter(injectors, inj => isPresent(inj) && isBlank(inj.parent));
}
static _textNodes(elements, binders) {
var textNodes = [];
for (var i = 0; i < binders.length; ++i) {
ProtoView._collectTextNodes(textNodes, elements[i],
binders[i].textNodeIndices);
}
return textNodes;
}
static _bindElements(elements, binders):List<Element> {
var bindElements = [];
for (var i = 0; i < binders.length; ++i) {
if (binders[i].hasElementPropertyBindings) ListWrapper.push(
bindElements, elements[i]);
}
return bindElements;
}
static _collectTextNodes(allTextNodes, element, indices) {
2014-11-19 14:54:07 -08:00
if (isPresent(indices)) {
var childNodes = DOM.templateAwareRoot(element).childNodes;
for (var i = 0; i < indices.length; ++i) {
ListWrapper.push(allTextNodes, childNodes[indices[i]]);
}
}
2014-09-28 20:02:32 -07:00
}
static _instantiateChildComponentViews(elements, binders, injectors,
shadowDomAppInjectors: List<Injector>, view: View) {
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);
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]);
}
}
}
}
static _createShadowAppInjectors(binders: List<ElementBinders>, lightDomAppInjector: Injector): List<Injectors> {
var injectors = ListWrapper.createFixedSize(binders.length);
for (var i = 0; i < binders.length; ++i) {
var componentDirective = binders[i].componentDirective;
if (isPresent(componentDirective)) {
var services = componentDirective.annotation.componentServices;
injectors[i] = isPresent(services) ?
lightDomAppInjector.createChild(services) : lightDomAppInjector;
} else {
injectors[i] = null;
}
}
return injectors;
}
// 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: AnnotatedType): ProtoView {
var rootProtoView = new ProtoView(insertionElement, new ProtoRecordRange());
var binder = rootProtoView.bindElement(
new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true));
binder.componentDirective = rootComponentAnnotatedType;
binder.nestedProtoView = protoView;
DOM.addClass(insertionElement, 'ng-binding');
return rootProtoView;
}
2014-09-28 20:02:32 -07:00
}
2014-10-10 20:44:55 -07:00
export class ElementPropertyMemento {
_elementIndex:int;
_propertyName:string;
2014-10-10 20:44:55 -07:00
constructor(elementIndex:int, propertyName:string) {
this._elementIndex = elementIndex;
this._propertyName = propertyName;
}
invoke(record:Record, bindElements:List<Element>) {
var element:Element = bindElements[this._elementIndex];
2014-10-10 20:44:55 -07:00
DOM.setProperty(element, this._propertyName, record.currentValue);
}
}
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);
}
}
//TODO(tbosch): I don't like to have done be called from a different place than notify
// notify is called by change detection, but done is called by our wrapper on detect changes.
export class OnChangeDispatcher {
_lastView:View;
_lastTarget:DirectivePropertyMemento;
2014-09-28 16:29:11 -07:00
constructor() {
this._lastView = null;
this._lastTarget = null;
}
2014-10-10 20:44:55 -07:00
notify(view:View, eTarget:DirectivePropertyMemento) {
2014-09-28 16:29:11 -07:00
}
done() {
}
}