angular2
Version:
Angular 2 - a web framework for modern web apps
342 lines (304 loc) • 14.4 kB
text/typescript
import {isPresent, isBlank, Type, isArray, isNumber} from 'angular2/src/facade/lang';
import {RenderProtoViewRef, RenderComponentTemplate} from 'angular2/src/core/render/api';
import {Optional, Injectable, Provider, resolveForwardRef, Inject} from 'angular2/src/core/di';
import {PipeProvider} from '../pipes/pipe_provider';
import {ProtoPipes} from '../pipes/pipes';
import {AppProtoView, AppProtoViewMergeInfo, ViewType} from './view';
import {ElementBinder} from './element_binder';
import {ProtoElementInjector, DirectiveProvider} from './element_injector';
import {DirectiveResolver} from './directive_resolver';
import {ViewResolver} from './view_resolver';
import {PipeResolver} from './pipe_resolver';
import {ViewMetadata, ViewEncapsulation} from '../metadata/view';
import {PLATFORM_PIPES} from 'angular2/src/core/platform_directives_and_pipes';
import {
visitAllCommands,
CompiledComponentTemplate,
CompiledHostTemplate,
TemplateCmd,
CommandVisitor,
EmbeddedTemplateCmd,
BeginComponentCmd,
BeginElementCmd,
IBeginElementCmd,
TextCmd,
NgContentCmd
} from './template_commands';
import {Renderer} from 'angular2/src/core/render/api';
import {APP_ID} from 'angular2/src/core/application_tokens';
export class ProtoViewFactory {
private _cache: Map<string, AppProtoView> = new Map<string, AppProtoView>();
private _nextTemplateId: number = 0;
constructor(private _renderer: Renderer,
private _platformPipes: Array<Type | any[]>,
private _directiveResolver: DirectiveResolver, private _viewResolver: ViewResolver,
private _pipeResolver: PipeResolver, private _appId: string) {}
clearCache() { this._cache.clear(); }
createHost(compiledHostTemplate: CompiledHostTemplate): AppProtoView {
var compiledTemplate = compiledHostTemplate.template;
var result = this._cache.get(compiledTemplate.id);
if (isBlank(result)) {
var emptyMap: {[key: string]: PipeProvider} = {};
var shortId = `${this._appId}-${this._nextTemplateId++}`;
this._renderer.registerComponentTemplate(new RenderComponentTemplate(
compiledTemplate.id, shortId, ViewEncapsulation.None, compiledTemplate.commands, []));
result =
new AppProtoView(compiledTemplate.id, compiledTemplate.commands, ViewType.HOST, true,
compiledTemplate.changeDetectorFactory, null, new ProtoPipes(emptyMap));
this._cache.set(compiledTemplate.id, result);
}
return result;
}
private _createComponent(cmd: BeginComponentCmd): AppProtoView {
var nestedProtoView = this._cache.get(cmd.templateId);
if (isBlank(nestedProtoView)) {
var component = cmd.directives[0];
var view = this._viewResolver.resolve(component);
var compiledTemplate = cmd.templateGetter();
var styles = _flattenStyleArr(compiledTemplate.styles, []);
var shortId = `${this._appId}-${this._nextTemplateId++}`;
this._renderer.registerComponentTemplate(new RenderComponentTemplate(
compiledTemplate.id, shortId, cmd.encapsulation, compiledTemplate.commands, styles));
var boundPipes = this._flattenPipes(view).map(pipe => this._bindPipe(pipe));
nestedProtoView = new AppProtoView(
compiledTemplate.id, compiledTemplate.commands, ViewType.COMPONENT, true,
compiledTemplate.changeDetectorFactory, null, ProtoPipes.fromProviders(boundPipes));
// Note: The cache is updated before recursing
// to be able to resolve cycles
this._cache.set(compiledTemplate.id, nestedProtoView);
this._initializeProtoView(nestedProtoView, null);
}
return nestedProtoView;
}
private _createEmbeddedTemplate(cmd: EmbeddedTemplateCmd, parent: AppProtoView): AppProtoView {
var nestedProtoView = new AppProtoView(
parent.templateId, cmd.children, ViewType.EMBEDDED, cmd.isMerged, cmd.changeDetectorFactory,
arrayToMap(cmd.variableNameAndValues, true), new ProtoPipes(parent.pipes.config));
if (cmd.isMerged) {
this.initializeProtoViewIfNeeded(nestedProtoView);
}
return nestedProtoView;
}
initializeProtoViewIfNeeded(protoView: AppProtoView) {
if (!protoView.isInitialized()) {
var render = this._renderer.createProtoView(protoView.templateId, protoView.templateCmds);
this._initializeProtoView(protoView, render);
}
}
private _initializeProtoView(protoView: AppProtoView, render: RenderProtoViewRef) {
var initializer = new _ProtoViewInitializer(protoView, this._directiveResolver, this);
visitAllCommands(initializer, protoView.templateCmds);
var mergeInfo =
new AppProtoViewMergeInfo(initializer.mergeEmbeddedViewCount, initializer.mergeElementCount,
initializer.mergeViewCount);
protoView.init(render, initializer.elementBinders, initializer.boundTextCount, mergeInfo,
initializer.variableLocations);
}
private _bindPipe(typeOrProvider): PipeProvider {
let meta = this._pipeResolver.resolve(typeOrProvider);
return PipeProvider.createFromType(typeOrProvider, meta);
}
private _flattenPipes(view: ViewMetadata): any[] {
let pipes = [];
if (isPresent(this._platformPipes)) {
_flattenArray(this._platformPipes, pipes);
}
if (isPresent(view.pipes)) {
_flattenArray(view.pipes, pipes);
}
return pipes;
}
}
function createComponent(protoViewFactory: ProtoViewFactory, cmd: BeginComponentCmd): AppProtoView {
return (<any>protoViewFactory)._createComponent(cmd);
}
function createEmbeddedTemplate(protoViewFactory: ProtoViewFactory, cmd: EmbeddedTemplateCmd,
parent: AppProtoView): AppProtoView {
return (<any>protoViewFactory)._createEmbeddedTemplate(cmd, parent);
}
class _ProtoViewInitializer implements CommandVisitor {
variableLocations: Map<string, number> = new Map<string, number>();
boundTextCount: number = 0;
boundElementIndex: number = 0;
elementBinderStack: ElementBinder[] = [];
distanceToParentElementBinder: number = 0;
distanceToParentProtoElementInjector: number = 0;
elementBinders: ElementBinder[] = [];
mergeEmbeddedViewCount: number = 0;
mergeElementCount: number = 0;
mergeViewCount: number = 1;
constructor(private _protoView: AppProtoView, private _directiveResolver: DirectiveResolver,
private _protoViewFactory: ProtoViewFactory) {}
visitText(cmd: TextCmd, context: any): any {
if (cmd.isBound) {
this.boundTextCount++;
}
return null;
}
visitNgContent(cmd: NgContentCmd, context: any): any { return null; }
visitBeginElement(cmd: BeginElementCmd, context: any): any {
if (cmd.isBound) {
this._visitBeginBoundElement(cmd, null);
} else {
this._visitBeginElement(cmd, null, null);
}
return null;
}
visitEndElement(context: any): any { return this._visitEndElement(); }
visitBeginComponent(cmd: BeginComponentCmd, context: any): any {
var nestedProtoView = createComponent(this._protoViewFactory, cmd);
return this._visitBeginBoundElement(cmd, nestedProtoView);
}
visitEndComponent(context: any): any { return this._visitEndElement(); }
visitEmbeddedTemplate(cmd: EmbeddedTemplateCmd, context: any): any {
var nestedProtoView = createEmbeddedTemplate(this._protoViewFactory, cmd, this._protoView);
if (cmd.isMerged) {
this.mergeEmbeddedViewCount++;
}
this._visitBeginBoundElement(cmd, nestedProtoView);
return this._visitEndElement();
}
private _visitBeginBoundElement(cmd: IBeginElementCmd, nestedProtoView: AppProtoView): any {
if (isPresent(nestedProtoView) && nestedProtoView.isMergable) {
this.mergeElementCount += nestedProtoView.mergeInfo.elementCount;
this.mergeViewCount += nestedProtoView.mergeInfo.viewCount;
this.mergeEmbeddedViewCount += nestedProtoView.mergeInfo.embeddedViewCount;
}
var elementBinder = _createElementBinder(
this._directiveResolver, nestedProtoView, this.elementBinderStack, this.boundElementIndex,
this.distanceToParentElementBinder, this.distanceToParentProtoElementInjector, cmd);
this.elementBinders.push(elementBinder);
var protoElementInjector = elementBinder.protoElementInjector;
for (var i = 0; i < cmd.variableNameAndValues.length; i += 2) {
this.variableLocations.set(<string>cmd.variableNameAndValues[i], this.boundElementIndex);
}
this.boundElementIndex++;
this.mergeElementCount++;
return this._visitBeginElement(cmd, elementBinder, protoElementInjector);
}
private _visitBeginElement(cmd: IBeginElementCmd, elementBinder: ElementBinder,
protoElementInjector: ProtoElementInjector): any {
this.distanceToParentElementBinder =
isPresent(elementBinder) ? 1 : this.distanceToParentElementBinder + 1;
this.distanceToParentProtoElementInjector =
isPresent(protoElementInjector) ? 1 : this.distanceToParentProtoElementInjector + 1;
this.elementBinderStack.push(elementBinder);
return null;
}
private _visitEndElement(): any {
var parentElementBinder = this.elementBinderStack.pop();
var parentProtoElementInjector =
isPresent(parentElementBinder) ? parentElementBinder.protoElementInjector : null;
this.distanceToParentElementBinder = isPresent(parentElementBinder) ?
parentElementBinder.distanceToParent :
this.distanceToParentElementBinder - 1;
this.distanceToParentProtoElementInjector = isPresent(parentProtoElementInjector) ?
parentProtoElementInjector.distanceToParent :
this.distanceToParentProtoElementInjector - 1;
return null;
}
}
function _createElementBinder(directiveResolver: DirectiveResolver, nestedProtoView: AppProtoView,
elementBinderStack: ElementBinder[], boundElementIndex: number,
distanceToParentBinder: number, distanceToParentPei: number,
beginElementCmd: IBeginElementCmd): ElementBinder {
var parentElementBinder: ElementBinder = null;
var parentProtoElementInjector: ProtoElementInjector = null;
if (distanceToParentBinder > 0) {
parentElementBinder = elementBinderStack[elementBinderStack.length - distanceToParentBinder];
}
if (isBlank(parentElementBinder)) {
distanceToParentBinder = -1;
}
if (distanceToParentPei > 0) {
var peiBinder = elementBinderStack[elementBinderStack.length - distanceToParentPei];
if (isPresent(peiBinder)) {
parentProtoElementInjector = peiBinder.protoElementInjector;
}
}
if (isBlank(parentProtoElementInjector)) {
distanceToParentPei = -1;
}
var componentDirectiveProvider: DirectiveProvider = null;
var isEmbeddedTemplate = false;
var directiveProviders: DirectiveProvider[] =
beginElementCmd.directives.map(type => provideDirective(directiveResolver, type));
if (beginElementCmd instanceof BeginComponentCmd) {
componentDirectiveProvider = directiveProviders[0];
} else if (beginElementCmd instanceof EmbeddedTemplateCmd) {
isEmbeddedTemplate = true;
}
var protoElementInjector = null;
// Create a protoElementInjector for any element that either has bindings *or* has one
// or more var- defined *or* for <template> elements:
// - Elements with a var- defined need a their own element injector
// so that, when hydrating, $implicit can be set to the element.
// - <template> elements need their own ElementInjector so that we can query their TemplateRef
var hasVariables = beginElementCmd.variableNameAndValues.length > 0;
if (directiveProviders.length > 0 || hasVariables || isEmbeddedTemplate) {
var directiveVariableBindings = new Map<string, number>();
if (!isEmbeddedTemplate) {
directiveVariableBindings = createDirectiveVariableBindings(
beginElementCmd.variableNameAndValues, directiveProviders);
}
protoElementInjector = ProtoElementInjector.create(
parentProtoElementInjector, boundElementIndex, directiveProviders,
isPresent(componentDirectiveProvider), distanceToParentPei, directiveVariableBindings);
protoElementInjector.attributes = arrayToMap(beginElementCmd.attrNameAndValues, false);
}
return new ElementBinder(boundElementIndex, parentElementBinder, distanceToParentBinder,
protoElementInjector, componentDirectiveProvider, nestedProtoView);
}
function provideDirective(directiveResolver: DirectiveResolver, type: Type): DirectiveProvider {
let annotation = directiveResolver.resolve(type);
return DirectiveProvider.createFromType(type, annotation);
}
export function createDirectiveVariableBindings(
variableNameAndValues: Array<string | number>,
directiveProviders: DirectiveProvider[]): Map<string, number> {
var directiveVariableBindings = new Map<string, number>();
for (var i = 0; i < variableNameAndValues.length; i += 2) {
var templateName = <string>variableNameAndValues[i];
var dirIndex = <number>variableNameAndValues[i + 1];
if (isNumber(dirIndex)) {
directiveVariableBindings.set(templateName, dirIndex);
} else {
// a variable without a directive index -> reference the element
directiveVariableBindings.set(templateName, null);
}
}
return directiveVariableBindings;
}
function arrayToMap(arr: string[], inverse: boolean): Map<string, string> {
var result = new Map<string, string>();
for (var i = 0; i < arr.length; i += 2) {
if (inverse) {
result.set(arr[i + 1], arr[i]);
} else {
result.set(arr[i], arr[i + 1]);
}
}
return result;
}
function _flattenArray(tree: any[], out: Array<Type | Provider | any[]>): void {
for (var i = 0; i < tree.length; i++) {
var item = resolveForwardRef(tree[i]);
if (isArray(item)) {
_flattenArray(item, out);
} else {
out.push(item);
}
}
}
function _flattenStyleArr(arr: Array<string | any[]>, out: string[]): string[] {
for (var i = 0; i < arr.length; i++) {
var entry = arr[i];
if (isArray(entry)) {
_flattenStyleArr(<any[]>entry, out);
} else {
out.push(<string>entry);
}
}
return out;
}