feat(compiler): add change detector generation

Runtime and Codegen.

Part of #3605
Closes #4057
This commit is contained in:
Tobias Bosch
2015-09-08 09:57:51 -07:00
parent 2daf2eedb6
commit 12dd44f7f6
15 changed files with 373 additions and 120 deletions
@@ -12,7 +12,6 @@ import {
TestComponentBuilder
} 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';
@@ -23,7 +22,6 @@ import {
ChangeDetectorDefinition,
ChangeDetectorGenConfig,
DynamicProtoChangeDetector,
ProtoChangeDetector,
ChangeDetectionStrategy,
ChangeDispatcher,
DirectiveIndex,
@@ -33,6 +31,7 @@ import {
} 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';
import {TestContext, TestDirective, TestDispatcher, TestPipes} from './change_detector_mocks';
export function main() {
describe('ChangeDefinitionFactory', () => {
@@ -62,8 +61,8 @@ export function main() {
protoViewIndex: number = 0): ChangeDetector {
var protoChangeDetectors =
createChangeDetectorDefinitions(
new TypeMetadata({typeName: 'SomeComp'}), ChangeDetectionStrategy.CheckAlways,
new ChangeDetectorGenConfig(true, true, false),
new TypeMetadata({typeName: 'SomeComp'}), ChangeDetectionStrategy.Default,
new ChangeDetectorGenConfig(true, true, false, false),
parser.parse(domParser.parse(template, 'TestComp'), directives))
.map(definition => new DynamicProtoChangeDetector(definition));
var changeDetector = protoChangeDetectors[protoViewIndex].instantiate(dispatcher);
@@ -164,47 +163,3 @@ export function main() {
});
});
}
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; }
}
@@ -0,0 +1,138 @@
import {
ddescribe,
describe,
xdescribe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
AsyncTestCompleter,
inject
} from 'angular2/test_lib';
import {IS_DART} from '../platform';
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
import {MapWrapper} from 'angular2/src/core/facade/collection';
import {Promise} from 'angular2/src/core/facade/async';
import {ChangeDetectionCompiler} from 'angular2/src/compiler/change_detector_compiler';
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {
DirectiveMetadata,
TypeMetadata,
ChangeDetectionMetadata,
SourceModule
} from 'angular2/src/compiler/api';
import {MockSchemaRegistry} from './template_parser_spec';
import {TemplateParser} from 'angular2/src/compiler/template_parser';
import {
Parser,
Lexer,
ChangeDetectorGenConfig,
ChangeDetectionStrategy,
ChangeDispatcher,
DirectiveIndex,
Locals,
BindingTarget,
ChangeDetector
} from 'angular2/src/core/change_detection/change_detection';
import {evalModule} from './eval_module';
import {TestContext, TestDispatcher, TestPipes} from './change_detector_mocks';
// Attention: These module names have to correspond to real modules!
const MODULE_NAME = 'angular2/test/compiler/change_detector_compiler_spec';
export function main() {
describe('ChangeDetectorCompiler', () => {
var domParser: HtmlParser;
var parser: TemplateParser;
function createCompiler(useJit: boolean): ChangeDetectionCompiler {
return new ChangeDetectionCompiler(new ChangeDetectorGenConfig(true, true, false, useJit));
}
beforeEach(() => {
domParser = new HtmlParser();
parser = new TemplateParser(
new Parser(new Lexer()),
new MockSchemaRegistry({'invalidProp': false}, {'mappedAttr': 'mappedProp'}));
});
describe('compileComponentRuntime', () => {
function detectChanges(compiler: ChangeDetectionCompiler, template: string,
directives: DirectiveMetadata[] = CONST_EXPR([])): string[] {
var type = new TypeMetadata({typeName: 'SomeComp'});
var parsedTemplate = parser.parse(domParser.parse(template, 'TestComp'), directives);
var factories =
compiler.compileComponentRuntime(type, ChangeDetectionStrategy.Default, parsedTemplate);
return testChangeDetector(factories[0]);
}
it('should watch element properties (no jit)', () => {
expect(detectChanges(createCompiler(false), '<div [el-prop]="someProp">'))
.toEqual(['elementProperty(elProp)=someValue']);
});
it('should watch element properties (jit)', () => {
expect(detectChanges(createCompiler(true), '<div [el-prop]="someProp">'))
.toEqual(['elementProperty(elProp)=someValue']);
});
});
describe('compileComponentCodeGen', () => {
function detectChanges(compiler: ChangeDetectionCompiler, template: string,
directives: DirectiveMetadata[] = CONST_EXPR([])): Promise<string[]> {
var type = new TypeMetadata({typeName: 'SomeComp'});
var parsedTemplate = parser.parse(domParser.parse(template, 'TestComp'), directives);
var sourceModule =
compiler.compileComponentCodeGen(type, ChangeDetectionStrategy.Default, parsedTemplate);
var testableModule = createTestableModule(sourceModule, 0);
return evalModule(testableModule.source, testableModule.imports, null);
}
it('should watch element properties', inject([AsyncTestCompleter], (async) => {
detectChanges(createCompiler(true), '<div [el-prop]="someProp">')
.then((value) => {
expect(value).toEqual(['elementProperty(elProp)=someValue']);
async.done();
});
}));
});
});
}
function createTestableModule(sourceModule: SourceModule, changeDetectorIndex: number):
SourceModule {
var testableSource;
var testableImports = [[MODULE_NAME, 'mocks']].concat(sourceModule.imports);
if (IS_DART) {
testableSource = `${sourceModule.source}
run(_) { return mocks.testChangeDetector(CHANGE_DETECTORS[${changeDetectorIndex}]); }`;
} else {
testableSource = `${sourceModule.source}
exports.run = function(_) { return mocks.testChangeDetector(CHANGE_DETECTORS[${changeDetectorIndex}]); }`;
}
return new SourceModule(null, testableSource, testableImports);
}
export function testChangeDetector(changeDetectorFactory: Function): string[] {
var dispatcher = new TestDispatcher([], []);
var cd = changeDetectorFactory(dispatcher);
var ctx = new TestContext();
ctx.someProp = 'someValue';
var locals = new Locals(null, MapWrapper.createFromStringMap({'someVar': null}));
cd.hydrate(ctx, locals, dispatcher, new TestPipes());
cd.detectChanges();
return dispatcher.log;
}
@@ -0,0 +1,52 @@
import {isBlank} from 'angular2/src/core/facade/lang';
import {Pipes} from 'angular2/src/core/change_detection/pipes';
import {
ProtoChangeDetector,
ChangeDispatcher,
DirectiveIndex,
BindingTarget
} from 'angular2/src/core/change_detection/change_detection';
export class TestContext {
eventLog: string[] = [];
someProp: string;
someProp2: string;
onEvent(value: string) { this.eventLog.push(value); }
}
export class TestDirective {
eventLog: string[] = [];
dirProp: string;
onEvent(value: string) { this.eventLog.push(value); }
}
export 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()); }
}
export class TestPipes implements Pipes {
get(type: string) { return null; }
}
@@ -65,7 +65,7 @@ export const PROP_NAME = 'propName';
* In this case, we expect `id` and `expression` to be the same string.
*/
export function getDefinition(id: string): TestDefinition {
var genConfig = new ChangeDetectorGenConfig(true, true, true);
var genConfig = new ChangeDetectorGenConfig(true, true, true, true);
var testDef = null;
if (StringMapWrapper.contains(_ExpressionWithLocals.availableDefinitions, id)) {
let val = StringMapWrapper.get(_ExpressionWithLocals.availableDefinitions, id);
@@ -121,7 +121,7 @@ export function getDefinition(id: string): TestDefinition {
[_DirectiveUpdating.recordNoCallbacks], genConfig);
testDef = new TestDefinition(id, cdDef, null);
} else if (id == "updateElementProduction") {
var genConfig = new ChangeDetectorGenConfig(false, false, false);
var genConfig = new ChangeDetectorGenConfig(false, false, false, true);
var records = _createBindingRecords("name");
let cdDef = new ChangeDetectorDefinition(id, null, [], records, [], [], genConfig);
testDef = new TestDefinition(id, cdDef, null);
@@ -167,7 +167,7 @@ class _ExpressionWithLocals {
var variableBindings = _convertLocalsToVariableBindings(this.locals);
var bindingRecords = _createBindingRecords(this._expression);
var directiveRecords = [];
var genConfig = new ChangeDetectorGenConfig(true, true, true);
var genConfig = new ChangeDetectorGenConfig(true, true, true, true);
return new ChangeDetectorDefinition('(empty id)', strategy, variableBindings, bindingRecords,
[], directiveRecords, genConfig);
}
@@ -231,7 +231,7 @@ class _ExpressionWithMode {
_createHostEventRecords("(host-event)='false'", dirRecordWithOnPush))
}
var genConfig = new ChangeDetectorGenConfig(true, true, true);
var genConfig = new ChangeDetectorGenConfig(true, true, true, true);
return new ChangeDetectorDefinition('(empty id)', this._strategy, variableBindings,
bindingRecords, eventRecords, directiveRecords, genConfig);
@@ -260,7 +260,7 @@ class _DirectiveUpdating {
createChangeDetectorDefinition(): ChangeDetectorDefinition {
var strategy = null;
var variableBindings = [];
var genConfig = new ChangeDetectorGenConfig(true, true, true);
var genConfig = new ChangeDetectorGenConfig(true, true, true, true);
return new ChangeDetectorDefinition('(empty id)', strategy, variableBindings,
this._bindingRecords, [], this._directiveRecords,
@@ -1512,7 +1512,8 @@ export function main() {
describe('logging property updates', () => {
beforeEachBindings(() => [
bind(ChangeDetection)
.toValue(new DynamicChangeDetection(new ChangeDetectorGenConfig(true, true, true)))
.toValue(
new DynamicChangeDetection(new ChangeDetectorGenConfig(true, true, true, false)))
]);
it('should reflect property values as attributes',