diff --git a/modules/angular2/src/compiler/api.ts b/modules/angular2/src/compiler/api.ts index 509add5563..0aa8fec0d9 100644 --- a/modules/angular2/src/compiler/api.ts +++ b/modules/angular2/src/compiler/api.ts @@ -20,18 +20,41 @@ export class ChangeDetectionMetadata { events: string[]; hostListeners: StringMap; hostProperties: StringMap; - 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, - hostProperties?: StringMap + hostProperties?: StringMap, + 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; } } diff --git a/modules/angular2/src/compiler/change_definition_factory.ts b/modules/angular2/src/compiler/change_definition_factory.ts new file mode 100644 index 0000000000..0eab35fcad --- /dev/null +++ b/modules/angular2/src/compiler/change_definition_factory.ts @@ -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}`; +} diff --git a/modules/angular2/src/compiler/template_ast.ts b/modules/angular2/src/compiler/template_ast.ts index 3292a441dc..38085f6674 100644 --- a/modules/angular2/src/compiler/template_ast.ts +++ b/modules/angular2/src/compiler/template_ast.ts @@ -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); } diff --git a/modules/angular2/test/compiler/change_definition_factory_spec.ts b/modules/angular2/test/compiler/change_definition_factory_spec.ts new file mode 100644 index 0000000000..d9a19bd890 --- /dev/null +++ b/modules/angular2/test/compiler/change_definition_factory_spec.ts @@ -0,0 +1,220 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + el, + expect, + iit, + inject, + it, + xit, + TestComponentBuilder, + asNativeElements, + By +} from 'angular2/test_lib'; + +import {MapWrapper} from 'angular2/src/core/facade/collection'; +import {isBlank} from 'angular2/src/core/facade/lang'; + +import {HtmlParser} from 'angular2/src/compiler/html_parser'; +import {DirectiveMetadata, TypeMetadata, ChangeDetectionMetadata} from 'angular2/src/compiler/api'; + +import {MockSchemaRegistry} from './template_parser_spec'; + +import {TemplateParser} from 'angular2/src/compiler/template_parser'; + +import { + Parser, + Lexer, + ChangeDetectorDefinition, + ChangeDetectorGenConfig, + DynamicProtoChangeDetector, + ProtoChangeDetector, + ChangeDetectionStrategy, + ChangeDispatcher, + DirectiveIndex, + Locals, + BindingTarget, + ChangeDetector +} from 'angular2/src/core/change_detection/change_detection'; + +import {Pipes} from 'angular2/src/core/change_detection/pipes'; + + +import {createChangeDetectorDefinitions} from 'angular2/src/compiler/change_definition_factory'; + +export function main() { + describe('ChangeDefinitionFactory', () => { + var domParser: HtmlParser; + var parser: TemplateParser; + var dispatcher: TestDispatcher; + var context: TestContext; + var directive: TestDirective; + var locals: Locals; + var pipes: Pipes; + var eventLocals: Locals; + + beforeEach(() => { + domParser = new HtmlParser(); + parser = new TemplateParser( + new Parser(new Lexer()), + new MockSchemaRegistry({'invalidProp': false}, {'mappedAttr': 'mappedProp'})); + context = new TestContext(); + directive = new TestDirective(); + dispatcher = new TestDispatcher([directive], []); + locals = new Locals(null, MapWrapper.createFromStringMap({'someVar': null})); + eventLocals = new Locals(null, MapWrapper.createFromStringMap({'$event': null})); + pipes = new TestPipes(); + }); + + function createChangeDetector(template: string, directives: DirectiveMetadata[], + protoViewIndex: number = 0): ChangeDetector { + var protoChangeDetectors = + createChangeDetectorDefinitions( + new TypeMetadata({typeName: 'SomeComp'}), ChangeDetectionStrategy.CheckAlways, + new ChangeDetectorGenConfig(true, true, false), + parser.parse(domParser.parse(template, 'TestComp'), directives)) + .map(definition => new DynamicProtoChangeDetector(definition)); + var changeDetector = protoChangeDetectors[protoViewIndex].instantiate(dispatcher); + changeDetector.hydrate(context, locals, dispatcher, pipes); + return changeDetector; + } + + it('should watch element properties', () => { + var changeDetector = createChangeDetector('
', [], 0); + + context.someProp = 'someValue'; + changeDetector.detectChanges(); + expect(dispatcher.log).toEqual(['elementProperty(elProp)=someValue']); + }); + + it('should watch text nodes', () => { + var changeDetector = createChangeDetector('{{someProp}}', [], 0); + + context.someProp = 'someValue'; + changeDetector.detectChanges(); + expect(dispatcher.log).toEqual(['textNode(null)=someValue']); + }); + + it('should handle events', () => { + var changeDetector = createChangeDetector('
', [], 0); + + eventLocals.set('$event', 'click'); + changeDetector.handleEvent('click', 0, eventLocals); + expect(context.eventLog).toEqual(['click']); + }); + + it('should watch variables', () => { + var changeDetector = createChangeDetector('
', [], 0); + + locals.set('someVar', 'someValue'); + changeDetector.detectChanges(); + expect(dispatcher.log).toEqual(['elementProperty(elProp)=someValue']); + }); + + it('should write directive properties', () => { + var dirMeta = new DirectiveMetadata({ + type: new TypeMetadata({typeName: 'SomeDir'}), + selector: 'div', + changeDetection: new ChangeDetectionMetadata({properties: ['dirProp']}) + }); + + var changeDetector = createChangeDetector('
', [dirMeta], 0); + + context.someProp = 'someValue'; + changeDetector.detectChanges(); + expect(directive.dirProp).toEqual('someValue'); + }); + + it('should watch directive host properties', () => { + var dirMeta = new DirectiveMetadata({ + type: new TypeMetadata({typeName: 'SomeDir'}), + selector: 'div', + changeDetection: new ChangeDetectionMetadata({hostProperties: {'elProp': 'dirProp'}}) + }); + + var changeDetector = createChangeDetector('
', [dirMeta], 0); + + directive.dirProp = 'someValue'; + changeDetector.detectChanges(); + expect(dispatcher.log).toEqual(['elementProperty(elProp)=someValue']); + }); + + it('should handle directive events', () => { + var dirMeta = new DirectiveMetadata({ + type: new TypeMetadata({typeName: 'SomeDir'}), + selector: 'div', + changeDetection: + new ChangeDetectionMetadata({hostListeners: {'click': 'onEvent($event)'}}) + }); + + var changeDetector = createChangeDetector('
', [dirMeta], 0); + + eventLocals.set('$event', 'click'); + changeDetector.handleEvent('click', 0, eventLocals); + expect(directive.eventLog).toEqual(['click']); + }); + + it('should create change detectors for embedded templates', () => { + var changeDetector = createChangeDetector('