feat(compiler): allow to create ChangeDetectors from parsed templates

Part of #3605
Closes #3950
This commit is contained in:
Tobias Bosch
2015-09-01 16:24:23 -07:00
parent 5c9613e084
commit 2fea0c2602
5 changed files with 530 additions and 39 deletions

View File

@ -20,18 +20,41 @@ export class ChangeDetectionMetadata {
events: string[];
hostListeners: StringMap<string, string>;
hostProperties: StringMap<string, string>;
constructor({changeDetection, properties, events, hostListeners, hostProperties}: {
callAfterContentInit: boolean;
callAfterContentChecked: boolean;
callAfterViewInit: boolean;
callAfterViewChecked: boolean;
callOnChanges: boolean;
callDoCheck: boolean;
callOnInit: boolean;
constructor({changeDetection, properties, events, hostListeners, hostProperties,
callAfterContentInit, callAfterContentChecked, callAfterViewInit,
callAfterViewChecked, callOnChanges, callDoCheck, callOnInit}: {
changeDetection?: ChangeDetectionStrategy,
properties?: string[],
events?: string[],
hostListeners?: StringMap<string, string>,
hostProperties?: StringMap<string, string>
hostProperties?: StringMap<string, string>,
callAfterContentInit?: boolean,
callAfterContentChecked?: boolean,
callAfterViewInit?: boolean,
callAfterViewChecked?: boolean,
callOnChanges?: boolean,
callDoCheck?: boolean,
callOnInit?: boolean
}) {
this.changeDetection = changeDetection;
this.properties = properties;
this.events = events;
this.hostListeners = hostListeners;
this.hostProperties = hostProperties;
this.callAfterContentInit = callAfterContentInit;
this.callAfterContentChecked = callAfterContentChecked;
this.callAfterViewInit = callAfterViewInit;
this.callAfterViewChecked = callAfterViewChecked;
this.callOnChanges = callOnChanges;
this.callDoCheck = callDoCheck;
this.callOnInit = callOnInit;
}
}

View File

@ -0,0 +1,224 @@
import {ListWrapper} from 'angular2/src/core/facade/collection';
import {isPresent, isBlank} from 'angular2/src/core/facade/lang';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {
ChangeDetection,
DirectiveIndex,
BindingRecord,
DirectiveRecord,
ProtoChangeDetector,
ChangeDetectionStrategy,
ChangeDetectorDefinition,
ChangeDetectorGenConfig,
ASTWithSource
} from 'angular2/src/core/change_detection/change_detection';
import {DirectiveMetadata, TypeMetadata} from './api';
import {
TemplateAst,
ElementAst,
BoundTextAst,
PropertyBindingType,
DirectiveAst,
TemplateAstVisitor,
templateVisitAll,
NgContentAst,
EmbeddedTemplateAst,
VariableAst,
BoundElementPropertyAst,
BoundEventAst,
BoundDirectivePropertyAst,
AttrAst,
TextAst
} from './template_ast';
export function createChangeDetectorDefinitions(
componentType: TypeMetadata, componentStrategy: ChangeDetectionStrategy,
genConfig: ChangeDetectorGenConfig, parsedTemplate: TemplateAst[]): ChangeDetectorDefinition[] {
var visitor = new ProtoViewVisitor(componentStrategy);
templateVisitAll(visitor, parsedTemplate);
return createChangeDefinitions(visitor.allProtoViews, componentType, genConfig);
}
class ProtoViewVisitor implements TemplateAstVisitor {
viewCount: number = 0;
protoViewStack: ProtoViewVisitorData[] = [];
allProtoViews: ProtoViewVisitorData[] = [];
constructor(componentStrategy: ChangeDetectionStrategy) {
this._beginProtoView(new ProtoViewVisitorData(null, componentStrategy, this.viewCount++));
}
private _beginProtoView(data: ProtoViewVisitorData) {
this.protoViewStack.push(data);
this.allProtoViews.push(data);
}
get currentProtoView(): ProtoViewVisitorData {
return this.protoViewStack[this.protoViewStack.length - 1];
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
this.currentProtoView.boundElementCount++;
templateVisitAll(this, ast.directives);
this.viewCount++;
this._beginProtoView(new ProtoViewVisitorData(
this.currentProtoView, ChangeDetectionStrategy.Default, this.viewCount - 1));
// Attention: variables present on an embedded template count towards
// the embedded template and not the template anchor!
templateVisitAll(this, ast.vars);
templateVisitAll(this, ast.children);
this.protoViewStack.pop();
return null;
}
visitElement(ast: ElementAst, context: any): any {
if (ast.isBound()) {
this.currentProtoView.boundElementCount++;
}
templateVisitAll(this, ast.properties, null);
templateVisitAll(this, ast.events);
templateVisitAll(this, ast.vars);
for (var i = 0; i < ast.directives.length; i++) {
ast.directives[i].visit(this, i);
}
templateVisitAll(this, ast.children);
return null;
}
visitNgContent(ast: NgContentAst, context: any): any { return null; }
visitVariable(ast: VariableAst, context: any): any {
this.currentProtoView.variableNames.push(ast.name);
return null;
}
visitEvent(ast: BoundEventAst, directiveRecord: DirectiveRecord): any {
var bindingRecord =
isPresent(directiveRecord) ?
BindingRecord.createForHostEvent(ast.handler, ast.name, directiveRecord) :
BindingRecord.createForEvent(ast.handler, ast.name,
this.currentProtoView.boundElementCount - 1);
this.currentProtoView.eventRecords.push(bindingRecord);
return null;
}
visitElementProperty(ast: BoundElementPropertyAst, directiveRecord: DirectiveRecord): any {
var boundElementIndex = this.currentProtoView.boundElementCount - 1;
var dirIndex = isPresent(directiveRecord) ? directiveRecord.directiveIndex : null;
var bindingRecord;
if (ast.type === PropertyBindingType.Property) {
bindingRecord =
isPresent(dirIndex) ?
BindingRecord.createForHostProperty(dirIndex, ast.value, ast.name) :
BindingRecord.createForElementProperty(ast.value, boundElementIndex, ast.name);
} else if (ast.type === PropertyBindingType.Attribute) {
bindingRecord =
isPresent(dirIndex) ?
BindingRecord.createForHostAttribute(dirIndex, ast.value, ast.name) :
BindingRecord.createForElementAttribute(ast.value, boundElementIndex, ast.name);
} else if (ast.type === PropertyBindingType.Class) {
bindingRecord =
isPresent(dirIndex) ?
BindingRecord.createForHostClass(dirIndex, ast.value, ast.name) :
BindingRecord.createForElementClass(ast.value, boundElementIndex, ast.name);
} else if (ast.type === PropertyBindingType.Style) {
bindingRecord =
isPresent(dirIndex) ?
BindingRecord.createForHostStyle(dirIndex, ast.value, ast.name, ast.unit) :
BindingRecord.createForElementStyle(ast.value, boundElementIndex, ast.name, ast.unit);
}
this.currentProtoView.bindingRecords.push(bindingRecord);
return null;
}
visitAttr(ast: AttrAst, context: any): any { return null; }
visitBoundText(ast: BoundTextAst, context: any): any {
var boundTextIndex = this.currentProtoView.boundTextCount++;
this.currentProtoView.bindingRecords.push(
BindingRecord.createForTextNode(ast.value, boundTextIndex));
return null;
}
visitText(ast: TextAst, context: any): any { return null; }
visitDirective(ast: DirectiveAst, directiveIndexAsNumber: number): any {
var directiveIndex =
new DirectiveIndex(this.currentProtoView.boundElementCount - 1, directiveIndexAsNumber);
var directiveMetadata = ast.directive;
var changeDetectionMeta = directiveMetadata.changeDetection;
var directiveRecord = new DirectiveRecord({
directiveIndex: directiveIndex,
callAfterContentInit: changeDetectionMeta.callAfterContentInit,
callAfterContentChecked: changeDetectionMeta.callAfterContentChecked,
callAfterViewInit: changeDetectionMeta.callAfterViewInit,
callAfterViewChecked: changeDetectionMeta.callAfterViewChecked,
callOnChanges: changeDetectionMeta.callOnChanges,
callDoCheck: changeDetectionMeta.callDoCheck,
callOnInit: changeDetectionMeta.callOnInit,
changeDetection: changeDetectionMeta.changeDetection
});
this.currentProtoView.directiveRecords.push(directiveRecord);
templateVisitAll(this, ast.properties, directiveRecord);
var bindingRecords = this.currentProtoView.bindingRecords;
if (directiveRecord.callOnChanges) {
bindingRecords.push(BindingRecord.createDirectiveOnChanges(directiveRecord));
}
if (directiveRecord.callOnInit) {
bindingRecords.push(BindingRecord.createDirectiveOnInit(directiveRecord));
}
if (directiveRecord.callDoCheck) {
bindingRecords.push(BindingRecord.createDirectiveDoCheck(directiveRecord));
}
templateVisitAll(this, ast.hostProperties, directiveRecord);
templateVisitAll(this, ast.hostEvents, directiveRecord);
return null;
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, directiveRecord: DirectiveRecord): any {
// TODO: these setters should eventually be created by change detection, to make
// it monomorphic!
var setter = reflector.setter(ast.directiveName);
this.currentProtoView.bindingRecords.push(
BindingRecord.createForDirective(ast.value, ast.directiveName, setter, directiveRecord));
return null;
}
}
class ProtoViewVisitorData {
boundTextCount: number = 0;
boundElementCount: number = 0;
variableNames: string[] = [];
bindingRecords: BindingRecord[] = [];
eventRecords: BindingRecord[] = [];
directiveRecords: DirectiveRecord[] = [];
constructor(public parent: ProtoViewVisitorData, public strategy: ChangeDetectionStrategy,
public viewIndex: number) {}
}
function createChangeDefinitions(pvDatas: ProtoViewVisitorData[], componentType: TypeMetadata,
genConfig: ChangeDetectorGenConfig): ChangeDetectorDefinition[] {
var pvVariableNames = _collectNestedProtoViewsVariableNames(pvDatas);
return pvDatas.map(pvData => {
var viewType = pvData.viewIndex === 0 ? 'component' : 'embedded';
var id = _protoViewId(componentType, pvData.viewIndex, viewType);
return new ChangeDetectorDefinition(id, pvData.strategy, pvVariableNames[pvData.viewIndex],
pvData.bindingRecords, pvData.eventRecords,
pvData.directiveRecords, genConfig);
});
}
function _collectNestedProtoViewsVariableNames(pvs: ProtoViewVisitorData[]): string[][] {
var nestedPvVariableNames: string[][] = ListWrapper.createFixedSize(pvs.length);
pvs.forEach((pv) => {
var parentVariableNames: string[] =
isPresent(pv.parent) ? nestedPvVariableNames[pv.parent.viewIndex] : [];
nestedPvVariableNames[pv.viewIndex] = parentVariableNames.concat(pv.variableNames);
});
return nestedPvVariableNames;
}
function _protoViewId(hostComponentType: TypeMetadata, pvIndex: number, viewType: string): string {
return `${hostComponentType.typeName}_${viewType}_${pvIndex}`;
}

View File

@ -4,39 +4,47 @@ import {DirectiveMetadata} from './api';
export interface TemplateAst {
sourceInfo: string;
visit(visitor: TemplateAstVisitor): any;
visit(visitor: TemplateAstVisitor, context: any): any;
}
export class TextAst implements TemplateAst {
constructor(public value: string, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitText(this); }
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
export class BoundTextAst implements TemplateAst {
constructor(public value: AST, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitBoundText(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitBoundText(this, context);
}
}
export class AttrAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitAttr(this); }
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
}
export class BoundElementPropertyAst implements TemplateAst {
constructor(public name: string, public type: PropertyBindingType, public value: AST,
public unit: string, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitElementProperty(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElementProperty(this, context);
}
}
export class BoundEventAst implements TemplateAst {
constructor(public name: string, public target: string, public handler: AST,
public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitEvent(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEvent(this, context);
}
}
export class VariableAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitVariable(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitVariable(this, context);
}
}
export class ElementAst implements TemplateAst {
@ -44,32 +52,47 @@ export class ElementAst implements TemplateAst {
public events: BoundEventAst[], public vars: VariableAst[],
public directives: DirectiveAst[], public children: TemplateAst[],
public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitElement(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElement(this, context);
}
isBound(): boolean {
return (this.properties.length > 0 || this.events.length > 0 || this.vars.length > 0 ||
this.directives.length > 0);
}
}
export class EmbeddedTemplateAst implements TemplateAst {
constructor(public attrs: AttrAst[], public vars: VariableAst[],
public directives: DirectiveAst[], public children: TemplateAst[],
public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitEmbeddedTemplate(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEmbeddedTemplate(this, context);
}
}
export class BoundDirectivePropertyAst implements TemplateAst {
constructor(public directiveName: string, public templateName: string, public value: AST,
public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitDirectiveProperty(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirectiveProperty(this, context);
}
}
export class DirectiveAst implements TemplateAst {
constructor(public directive: DirectiveMetadata, public properties: BoundDirectivePropertyAst[],
public hostProperties: BoundElementPropertyAst[], public hostEvents: BoundEventAst[],
public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitDirective(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirective(this, context);
}
}
export class NgContentAst implements TemplateAst {
constructor(public select: string, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor): any { return visitor.visitNgContent(this); }
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitNgContent(this, context);
}
}
export enum PropertyBindingType {
@ -80,24 +103,25 @@ export enum PropertyBindingType {
}
export interface TemplateAstVisitor {
visitNgContent(ast: NgContentAst): any;
visitEmbeddedTemplate(ast: EmbeddedTemplateAst): any;
visitElement(ast: ElementAst): any;
visitVariable(ast: VariableAst): any;
visitEvent(ast: BoundEventAst): any;
visitElementProperty(ast: BoundElementPropertyAst): any;
visitAttr(ast: AttrAst): any;
visitBoundText(ast: BoundTextAst): any;
visitText(ast: TextAst): any;
visitDirective(ast: DirectiveAst): any;
visitDirectiveProperty(ast: BoundDirectivePropertyAst): any;
visitNgContent(ast: NgContentAst, context: any): any;
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any;
visitElement(ast: ElementAst, context: any): any;
visitVariable(ast: VariableAst, context: any): any;
visitEvent(ast: BoundEventAst, context: any): any;
visitElementProperty(ast: BoundElementPropertyAst, context: any): any;
visitAttr(ast: AttrAst, context: any): any;
visitBoundText(ast: BoundTextAst, context: any): any;
visitText(ast: TextAst, context: any): any;
visitDirective(ast: DirectiveAst, context: any): any;
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any;
}
export function templateVisitAll(visitor: TemplateAstVisitor, asts: TemplateAst[]): any[] {
export function templateVisitAll(visitor: TemplateAstVisitor, asts: TemplateAst[],
context: any = null): any[] {
var result = [];
asts.forEach(ast => {
var astResult = ast.visit(visitor);
var astResult = ast.visit(visitor, context);
if (isPresent(astResult)) {
result.push(astResult);
}