Tobias Bosch 50098767fc refactor(render): use render layer fully
Introduces angular2/src/core/compiler/ViewFactory which
extracts ProtoView.instantiate and replaces ViewPool.

Note: This is a work in progress commit to unblock other commits.
There will be follow ups to add unit tests, remove TODOs, …
2015-04-08 20:51:31 -07:00

286 lines
9.5 KiB
JavaScript

import {isPresent, isBlank, 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 {setterFactory} from './property_setter_factory';
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;
constructor(rootElement) {
this.rootElement = rootElement;
this.elements = [];
this.isRootView = false;
this.variableBindings = MapWrapper.create();
}
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 = [];
ListWrapper.forEach(this.elements, (ebb) => {
var propertySetters = MapWrapper.create();
var eventLocalsAstSplitter = new EventLocalsAstSplitter();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => {
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;
ListWrapper.push(apiElementBinders, new api.ElementBinder({
index: ebb.index, parentIndex:parentIndex, distanceToParent:ebb.distanceToParent,
directives: apiDirectiveBinders,
nestedProtoView: nestedProtoView,
propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings,
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(ebb.eventBindings),
textBindings: ebb.textBindings,
readAttributes: ebb.readAttributes
}));
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(),
propertySetters: propertySetters
}));
});
return new api.ProtoView({
render: new directDomRenderer.DirectDomProtoViewRef(new ProtoView({
element: this.rootElement,
elementBinders: renderElementBinders,
isRootView: this.isRootView
})),
elementBinders: apiElementBinders,
variableBindings: this.variableBindings
});
}
}
export class ElementBinderBuilder {
element;
index:number;
parent:ElementBinderBuilder;
distanceToParent: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>;
readAttributes: Map<string, string>;
componentId: string;
constructor(index, element, description) {
this.element = element;
this.index = index;
this.parent = null;
this.distanceToParent = 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;
this.readAttributes = MapWrapper.create();
}
setParent(parent:ElementBinderBuilder, distanceToParent):ElementBinderBuilder {
this.parent = parent;
if (isPresent(parent)) {
this.distanceToParent = distanceToParent;
}
return this;
}
readAttribute(attrName:string) {
if (isBlank(MapWrapper.get(this.readAttributes, attrName))) {
MapWrapper.set(this.readAttributes, attrName, DOM.getAttribute(this.element, attrName));
}
}
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);
this.bindPropertySetter(name);
}
bindPropertySetter(name) {
MapWrapper.set(this.propertySetters, name, setterFactory(name));
}
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;
}
setComponentId(componentId:string) {
this.componentId = componentId;
}
}
export class DirectiveBuilder {
directiveIndex:number;
propertyBindings: Map<string, ASTWithSource>;
eventBindings: Map<string, ASTWithSource>;
constructor(directiveIndex) {
this.directiveIndex = directiveIndex;
this.propertyBindings = MapWrapper.create();
this.eventBindings = MapWrapper.create();
}
bindProperty(name, expression) {
MapWrapper.set(this.propertyBindings, name, expression);
}
bindEvent(name, expression) {
MapWrapper.set(this.eventBindings, name, expression);
}
}
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) => {
// TODO(tbosch): reenable this when we are parsing element properties
// out of action expressions
// var adjustedAst = astWithSource.ast.visit(this);
var adjustedAst = astWithSource.ast;
MapWrapper.set(result, eventName, new ASTWithSource(adjustedAst, astWithSource.source, ''));
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;
}
}