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

@ -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('<div [el-prop]="someProp">', [], 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('<div on-click="onEvent($event)">', [], 0);
eventLocals.set('$event', 'click');
changeDetector.handleEvent('click', 0, eventLocals);
expect(context.eventLog).toEqual(['click']);
});
it('should watch variables', () => {
var changeDetector = createChangeDetector('<div #some-var [el-prop]="someVar">', [], 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('<div [dir-prop]="someProp">', [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('<div>', [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('<div>', [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('<template>{{someProp}}<template>', [], 1);
context.someProp = 'someValue';
changeDetector.detectChanges();
expect(dispatcher.log).toEqual(['textNode(null)=someValue']);
});
it('should watch expressions after embedded templates', () => {
var changeDetector =
createChangeDetector('<template>{{someProp2}}</template>{{someProp}}', [], 0);
context.someProp = 'someValue';
changeDetector.detectChanges();
expect(dispatcher.log).toEqual(['textNode(null)=someValue']);
});
});
}
class TestContext {
eventLog: string[] = [];
someProp: string;
someProp2: string;
onEvent(value: string) { this.eventLog.push(value); }
}
class TestDirective {
eventLog: string[] = [];
dirProp: string;
onEvent(value: string) { this.eventLog.push(value); }
}
class TestDispatcher implements ChangeDispatcher {
log: string[];
constructor(public directives: any[], public detectors: ProtoChangeDetector[]) { this.clear(); }
getDirectiveFor(di: DirectiveIndex) { return this.directives[di.directiveIndex]; }
getDetectorFor(di: DirectiveIndex) { return this.detectors[di.directiveIndex]; }
clear() { this.log = []; }
notifyOnBinding(target: BindingTarget, value) {
this.log.push(`${target.mode}(${target.name})=${this._asString(value)}`);
}
logBindingUpdate(target, value) {}
notifyAfterContentChecked() {}
notifyAfterViewChecked() {}
getDebugContext(a, b) { return null; }
_asString(value) { return (isBlank(value) ? 'null' : value.toString()); }
}
class TestPipes implements Pipes {
get(type: string) { return null; }
}

View File

@ -667,11 +667,11 @@ export function humanizeTemplateAsts(templateAsts: TemplateAst[]): any[] {
class TemplateHumanizer implements TemplateAstVisitor {
result: any[] = [];
visitNgContent(ast: NgContentAst): any {
visitNgContent(ast: NgContentAst, context: any): any {
this.result.push([NgContentAst, ast.select, ast.sourceInfo]);
return null;
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst): any {
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
this.result.push([EmbeddedTemplateAst, ast.sourceInfo]);
templateVisitAll(this, ast.attrs);
templateVisitAll(this, ast.vars);
@ -679,7 +679,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
templateVisitAll(this, ast.children);
return null;
}
visitElement(ast: ElementAst): any {
visitElement(ast: ElementAst, context: any): any {
this.result.push([ElementAst, ast.sourceInfo]);
templateVisitAll(this, ast.attrs);
templateVisitAll(this, ast.properties);
@ -689,11 +689,11 @@ class TemplateHumanizer implements TemplateAstVisitor {
templateVisitAll(this, ast.children);
return null;
}
visitVariable(ast: VariableAst): any {
visitVariable(ast: VariableAst, context: any): any {
this.result.push([VariableAst, ast.name, ast.value, ast.sourceInfo]);
return null;
}
visitEvent(ast: BoundEventAst): any {
visitEvent(ast: BoundEventAst, context: any): any {
this.result.push([
BoundEventAst,
ast.name,
@ -703,7 +703,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
]);
return null;
}
visitElementProperty(ast: BoundElementPropertyAst): any {
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {
this.result.push([
BoundElementPropertyAst,
ast.type,
@ -714,26 +714,26 @@ class TemplateHumanizer implements TemplateAstVisitor {
]);
return null;
}
visitAttr(ast: AttrAst): any {
visitAttr(ast: AttrAst, context: any): any {
this.result.push([AttrAst, ast.name, ast.value, ast.sourceInfo]);
return null;
}
visitBoundText(ast: BoundTextAst): any {
visitBoundText(ast: BoundTextAst, context: any): any {
this.result.push([BoundTextAst, expressionUnparser.unparse(ast.value), ast.sourceInfo]);
return null;
}
visitText(ast: TextAst): any {
visitText(ast: TextAst, context: any): any {
this.result.push([TextAst, ast.value, ast.sourceInfo]);
return null;
}
visitDirective(ast: DirectiveAst): any {
visitDirective(ast: DirectiveAst, context: any): any {
this.result.push([DirectiveAst, ast.directive, ast.sourceInfo]);
templateVisitAll(this, ast.properties);
templateVisitAll(this, ast.hostProperties);
templateVisitAll(this, ast.hostEvents);
return null;
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst): any {
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {
this.result.push([
BoundDirectivePropertyAst,
ast.directiveName,
@ -744,7 +744,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
}
}
class MockSchemaRegistry implements ElementSchemaRegistry {
export class MockSchemaRegistry implements ElementSchemaRegistry {
constructor(public existingProperties: StringMap<string, boolean>,
public attrPropMapping: StringMap<string, string>) {}
hasProperty(tagName: string, property: string): boolean {