feat(change_detection): reimplement change detection
This commit is contained in:
parent
22653707d9
commit
9957c1338e
|
@ -7,7 +7,7 @@ import {
|
|||
Lexer,
|
||||
Parser,
|
||||
ChangeDetector,
|
||||
ProtoRecordRange,
|
||||
ProtoChangeDetector,
|
||||
ChangeDispatcher,
|
||||
} from 'change_detection/change_detection';
|
||||
|
||||
|
@ -102,8 +102,8 @@ function setUpChangeDetection(iterations) {
|
|||
var dispatcher = new DummyDispatcher();
|
||||
var parser = new Parser(new Lexer());
|
||||
|
||||
var parentProto = new ProtoRecordRange();
|
||||
var parentRange = parentProto.instantiate(dispatcher, MapWrapper.create());
|
||||
var parentProto = new ProtoChangeDetector();
|
||||
var parentCD = parentProto.instantiate(dispatcher, MapWrapper.create());
|
||||
|
||||
var astWithSource = [
|
||||
parser.parseBinding('field0', null),
|
||||
|
@ -119,12 +119,12 @@ function setUpChangeDetection(iterations) {
|
|||
];
|
||||
|
||||
function proto(i) {
|
||||
var prr = new ProtoRecordRange();
|
||||
prr.addRecordsFromAST(astWithSource[i % 10].ast, "memo", i, false);
|
||||
return prr;
|
||||
var pcd = new ProtoChangeDetector();
|
||||
pcd.addAst(astWithSource[i % 10].ast, "memo", i, false);
|
||||
return pcd;
|
||||
}
|
||||
|
||||
var prr = [
|
||||
var pcd = [
|
||||
proto(0),
|
||||
proto(1),
|
||||
proto(2),
|
||||
|
@ -142,13 +142,13 @@ function setUpChangeDetection(iterations) {
|
|||
var index = i % 10;
|
||||
obj.setField(index, i);
|
||||
|
||||
var rr = prr[index].instantiate(dispatcher, null);
|
||||
var rr = pcd[index].instantiate(dispatcher, null);
|
||||
rr.setContext(obj);
|
||||
|
||||
parentRange.addRange(rr);
|
||||
parentCD.addChild(rr);
|
||||
}
|
||||
|
||||
return new ChangeDetector(parentRange);
|
||||
return parentCD;
|
||||
}
|
||||
|
||||
export function main () {
|
||||
|
|
|
@ -125,7 +125,7 @@ export function main() {
|
|||
setupReflector();
|
||||
|
||||
var app;
|
||||
var changeDetector;
|
||||
var lifeCycle;
|
||||
var baselineRootTreeComponent;
|
||||
var count = 0;
|
||||
|
||||
|
@ -133,7 +133,7 @@ export function main() {
|
|||
// TODO: We need an initial value as otherwise the getter for data.value will fail
|
||||
// --> this should be already caught in change detection!
|
||||
app.initData = new TreeNode('', null, null);
|
||||
changeDetector.detectChanges();
|
||||
lifeCycle.tick();
|
||||
}
|
||||
|
||||
function profile(create, destroy, name) {
|
||||
|
@ -171,14 +171,15 @@ export function main() {
|
|||
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', '-'];
|
||||
|
||||
app.initData = buildTree(maxDepth, values, 0);
|
||||
changeDetector.detectChanges();
|
||||
lifeCycle.tick();
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
function initNg2() {
|
||||
bootstrap(AppComponent).then((injector) => {
|
||||
changeDetector = injector.get(ChangeDetector);
|
||||
lifeCycle = injector.get(LifeCycle);
|
||||
|
||||
app = injector.get(AppComponent);
|
||||
bindAction('#ng2DestroyDom', ng2DestroyDom);
|
||||
bindAction('#ng2CreateDom', ng2CreateDom);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
export {ChangeDetectionError, ChangeDetector} from './change_detector';
|
||||
export {AST, ASTWithSource} from './parser/ast';
|
||||
export {AST} from './parser/ast';
|
||||
export {Lexer} from './parser/lexer';
|
||||
export {Parser} from './parser/parser';
|
||||
export {ProtoRecordRange, RecordRange, ChangeDispatcher} from './record_range';
|
||||
export {ProtoRecord, Record} from './record';
|
||||
export {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
|
||||
|
||||
export {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
|
||||
export {ChangeRecord, ChangeDispatcher, ChangeDetector} from './interfaces';
|
||||
export {ProtoChangeDetector} from './proto_change_detector';
|
||||
export {DynamicChangeDetector} from './dynamic_change_detector';
|
|
@ -1,87 +0,0 @@
|
|||
import {ProtoRecordRange, RecordRange} from './record_range';
|
||||
import {ProtoRecord, Record} from './record';
|
||||
import {int, isPresent, isBlank} from 'facade/lang';
|
||||
import {ListWrapper, List} from 'facade/collection';
|
||||
|
||||
export * from './record';
|
||||
export * from './record_range'
|
||||
|
||||
class ExpressionChangedAfterItHasBeenChecked extends Error {
|
||||
message:string;
|
||||
|
||||
constructor(record:Record) {
|
||||
this.message = `Expression '${record.expressionAsString()}' has changed after it was checked. ` +
|
||||
`Previous value: '${record.previousValue}'. Current value: '${record.currentValue}'`;
|
||||
}
|
||||
|
||||
toString():string {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChangeDetector {
|
||||
_rootRecordRange:RecordRange;
|
||||
_enforceNoNewChanges:boolean;
|
||||
|
||||
constructor(recordRange:RecordRange, enforceNoNewChanges:boolean = false) {
|
||||
this._rootRecordRange = recordRange;
|
||||
this._enforceNoNewChanges = enforceNoNewChanges;
|
||||
}
|
||||
|
||||
detectChanges():int {
|
||||
var count = this._detectChanges(false);
|
||||
if (this._enforceNoNewChanges) {
|
||||
this._detectChanges(true)
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
_detectChanges(throwOnChange:boolean):int {
|
||||
var count = 0;
|
||||
var updatedRecords = null;
|
||||
var record = this._rootRecordRange.findFirstEnabledRecord();
|
||||
var currentRange, currentGroup;
|
||||
|
||||
while (isPresent(record)) {
|
||||
if (record.check()) {
|
||||
count++;
|
||||
if (record.terminatesExpression()) {
|
||||
if (throwOnChange) throw new ExpressionChangedAfterItHasBeenChecked(record);
|
||||
currentRange = record.recordRange;
|
||||
currentGroup = record.groupMemento();
|
||||
updatedRecords = this._addRecord(updatedRecords, record);
|
||||
}
|
||||
}
|
||||
|
||||
if (isPresent(updatedRecords)) {
|
||||
var nextEnabled = record.nextEnabled;
|
||||
if (isBlank(nextEnabled) || // we have reached the last enabled record
|
||||
currentRange !== nextEnabled.recordRange || // the next record is in a different range
|
||||
currentGroup !== nextEnabled.groupMemento()) { // the next record is in a different group
|
||||
currentRange.dispatcher.onRecordChange(currentGroup, updatedRecords);
|
||||
updatedRecords = null;
|
||||
}
|
||||
}
|
||||
|
||||
record = record.findNextEnabled();
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
_addRecord(updatedRecords:List, record:Record) {
|
||||
if (isBlank(updatedRecords)) {
|
||||
updatedRecords = _singleElementList;
|
||||
updatedRecords[0] = record;
|
||||
|
||||
} else if (updatedRecords === _singleElementList) {
|
||||
updatedRecords = [_singleElementList[0], record];
|
||||
|
||||
} else {
|
||||
ListWrapper.push(updatedRecords, record);
|
||||
}
|
||||
return updatedRecords;
|
||||
}
|
||||
}
|
||||
|
||||
var _singleElementList = [null];
|
|
@ -0,0 +1,259 @@
|
|||
import {isPresent, isBlank, BaseException, FunctionWrapper} from 'facade/lang';
|
||||
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
|
||||
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
|
||||
|
||||
import {ArrayChanges} from './array_changes';
|
||||
import {KeyValueChanges} from './keyvalue_changes';
|
||||
|
||||
import {
|
||||
ProtoRecord,
|
||||
RECORD_TYPE_SELF,
|
||||
RECORD_TYPE_PROPERTY,
|
||||
RECORD_TYPE_INVOKE_METHOD,
|
||||
RECORD_TYPE_CONST,
|
||||
RECORD_TYPE_INVOKE_CLOSURE,
|
||||
RECORD_TYPE_INVOKE_PURE_FUNCTION,
|
||||
RECORD_TYPE_INVOKE_FORMATTER,
|
||||
RECORD_TYPE_STRUCTURAL_CHECK,
|
||||
ProtoChangeDetector
|
||||
} from './proto_change_detector';
|
||||
|
||||
import {ChangeDetector, ChangeRecord, ChangeDispatcher} from './interfaces';
|
||||
import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './exceptions';
|
||||
|
||||
class SimpleChange {
|
||||
previousValue:any;
|
||||
currentValue:any;
|
||||
|
||||
constructor(previousValue:any, currentValue:any) {
|
||||
this.previousValue = previousValue;
|
||||
this.currentValue = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
export class DynamicChangeDetector extends ChangeDetector {
|
||||
dispatcher:any;
|
||||
formatters:Map;
|
||||
children:List;
|
||||
values:List;
|
||||
protos:List<ProtoRecord>;
|
||||
parent:ChangeDetector;
|
||||
|
||||
constructor(dispatcher:any, formatters:Map, protoRecords:List<ProtoRecord>) {
|
||||
this.dispatcher = dispatcher;
|
||||
this.formatters = formatters;
|
||||
this.values = ListWrapper.createFixedSize(protoRecords.length + 1);
|
||||
this.protos = protoRecords;
|
||||
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
addChild(cd:ChangeDetector) {
|
||||
ListWrapper.push(this.children, cd);
|
||||
cd.parent = this;
|
||||
}
|
||||
|
||||
removeChild(cd:ChangeDetector) {
|
||||
ListWrapper.remove(this.children, cd);
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.parent.removeChild(this);
|
||||
}
|
||||
|
||||
setContext(context:any) {
|
||||
this.values[0] = context;
|
||||
}
|
||||
|
||||
detectChanges() {
|
||||
this._detectChanges(false);
|
||||
}
|
||||
|
||||
checkNoChanges() {
|
||||
this._detectChanges(true);
|
||||
}
|
||||
|
||||
_detectChanges(throwOnChange:boolean) {
|
||||
this._detectChangesInRecords(throwOnChange);
|
||||
this._detectChangesInChildren(throwOnChange);
|
||||
}
|
||||
|
||||
_detectChangesInRecords(throwOnChange:boolean) {
|
||||
var protos:List<ProtoRecord> = this.protos;
|
||||
|
||||
var updatedRecords = null;
|
||||
var currentGroup = null;
|
||||
|
||||
for (var i = 0; i < protos.length; ++i) {
|
||||
var proto:ProtoRecord = protos[i];
|
||||
var change = this._check(proto);
|
||||
|
||||
// only when the terminal record, which ends a binding, changes
|
||||
// we need to add it to a list of changed records
|
||||
if (isPresent(change) && proto.terminal) {
|
||||
if (throwOnChange) throw new ExpressionChangedAfterItHasBeenChecked(proto, change);
|
||||
currentGroup = proto.groupMemento;
|
||||
updatedRecords = this._addRecord(updatedRecords, proto, change);
|
||||
}
|
||||
|
||||
if (isPresent(updatedRecords)) {
|
||||
var lastRecordOfCurrentGroup = protos.length == i + 1 ||
|
||||
currentGroup !== protos[i + 1].groupMemento;
|
||||
if (lastRecordOfCurrentGroup) {
|
||||
this.dispatcher.onRecordChange(currentGroup, updatedRecords);
|
||||
updatedRecords = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_check(proto:ProtoRecord) {
|
||||
try {
|
||||
if (proto.mode == RECORD_TYPE_STRUCTURAL_CHECK) {
|
||||
return this._structuralCheck(proto);
|
||||
} else {
|
||||
return this._referenceCheck(proto);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new ChangeDetectionError(proto, e);
|
||||
}
|
||||
}
|
||||
|
||||
_referenceCheck(proto) {
|
||||
var prevValue = this._readSelf(proto);
|
||||
var currValue = this._calculateCurrValue(proto);
|
||||
|
||||
if (! isSame(prevValue, currValue)) {
|
||||
this._writeSelf(proto, currValue);
|
||||
return new SimpleChange(prevValue, currValue);
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_calculateCurrValue(proto) {
|
||||
switch (proto.mode) {
|
||||
case RECORD_TYPE_SELF:
|
||||
throw new BaseException("Cannot evaluate self");
|
||||
|
||||
case RECORD_TYPE_CONST:
|
||||
return proto.funcOrValue;
|
||||
|
||||
case RECORD_TYPE_PROPERTY:
|
||||
var context = this._readContext(proto);
|
||||
while (context instanceof ContextWithVariableBindings) {
|
||||
if (context.hasBinding(proto.name)) {
|
||||
return context.get(proto.name);
|
||||
}
|
||||
context = context.parent;
|
||||
}
|
||||
var propertyGetter:Function = proto.funcOrValue;
|
||||
return propertyGetter(context);
|
||||
|
||||
case RECORD_TYPE_INVOKE_METHOD:
|
||||
var methodInvoker:Function = proto.funcOrValue;
|
||||
return methodInvoker(this._readContext(proto), this._readArgs(proto));
|
||||
|
||||
case RECORD_TYPE_INVOKE_CLOSURE:
|
||||
return FunctionWrapper.apply(this._readContext(proto), this._readArgs(proto));
|
||||
|
||||
case RECORD_TYPE_INVOKE_PURE_FUNCTION:
|
||||
return FunctionWrapper.apply(proto.funcOrValue, this._readArgs(proto));
|
||||
|
||||
case RECORD_TYPE_INVOKE_FORMATTER:
|
||||
var formatter = MapWrapper.get(this.formatters, proto.funcOrValue);
|
||||
return FunctionWrapper.apply(formatter, this._readArgs(proto));
|
||||
|
||||
default:
|
||||
throw new BaseException(`Unknown operation ${proto.mode}`);
|
||||
}
|
||||
}
|
||||
|
||||
_structuralCheck(proto) {
|
||||
var self = this._readSelf(proto);
|
||||
var context = this._readContext(proto);
|
||||
|
||||
if (isBlank(self)) {
|
||||
if (ArrayChanges.supports(context)) {
|
||||
self = new ArrayChanges();
|
||||
} else if (KeyValueChanges.supports(context)) {
|
||||
self = new KeyValueChanges();
|
||||
}
|
||||
}
|
||||
|
||||
if (ArrayChanges.supports(context)) {
|
||||
if (self.check(context)) {
|
||||
this._writeSelf(proto, self);
|
||||
return new SimpleChange(null, self); // TODO: don't wrap and return self instead
|
||||
}
|
||||
|
||||
} else if (KeyValueChanges.supports(context)) {
|
||||
if (self.check(context)) {
|
||||
this._writeSelf(proto, self);
|
||||
return new SimpleChange(null, self); // TODO: don't wrap and return self instead
|
||||
}
|
||||
|
||||
} else if (context == null) {
|
||||
this._writeSelf(proto, null);
|
||||
return new SimpleChange(null, null);
|
||||
|
||||
} else {
|
||||
throw new BaseException(`Unsupported type (${context})`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_addRecord(updatedRecords:List, proto:ProtoRecord, change):List {
|
||||
// we can use a pool of change records not to create extra garbage
|
||||
var record = new ChangeRecord(proto.bindingMemento, change);
|
||||
if (isBlank(updatedRecords)) {
|
||||
updatedRecords = _singleElementList;
|
||||
updatedRecords[0] = record;
|
||||
|
||||
} else if (updatedRecords === _singleElementList) {
|
||||
updatedRecords = [_singleElementList[0], record];
|
||||
|
||||
} else {
|
||||
ListWrapper.push(updatedRecords, record);
|
||||
}
|
||||
return updatedRecords;
|
||||
}
|
||||
|
||||
_detectChangesInChildren(throwOnChange:boolean) {
|
||||
var children = this.children;
|
||||
for(var i = 0; i < children.length; ++i) {
|
||||
children[i]._detectChanges(throwOnChange);
|
||||
}
|
||||
}
|
||||
|
||||
_readContext(proto) {
|
||||
return this.values[proto.contextIndex];
|
||||
}
|
||||
|
||||
_readSelf(proto) {
|
||||
return this.values[proto.record_type_selfIndex];
|
||||
}
|
||||
|
||||
_writeSelf(proto, value) {
|
||||
this.values[proto.record_type_selfIndex] = value;
|
||||
}
|
||||
|
||||
_readArgs(proto) {
|
||||
var res = ListWrapper.createFixedSize(proto.args.length);
|
||||
var args = proto.args;
|
||||
for (var i = 0; i < args.length; ++i) {
|
||||
res[i] = this.values[args[i]];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
var _singleElementList = [null];
|
||||
|
||||
function isSame(a, b) {
|
||||
if (a === b) return true;
|
||||
if (a instanceof String && b instanceof String && a == b) return true;
|
||||
if ((a !== a) && (b !== b)) return true;
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import {ProtoRecord} from './proto_change_detector';
|
||||
|
||||
export class ExpressionChangedAfterItHasBeenChecked extends Error {
|
||||
message:string;
|
||||
|
||||
constructor(proto:ProtoRecord, change:any) {
|
||||
this.message = `Expression '${proto.expressionAsString}' has changed after it was checked. ` +
|
||||
`Previous value: '${change.previousValue}'. Current value: '${change.currentValue}'`;
|
||||
}
|
||||
|
||||
toString():string {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChangeDetectionError extends Error {
|
||||
message:string;
|
||||
originalException:any;
|
||||
location:string;
|
||||
|
||||
constructor(proto:ProtoRecord, originalException:any) {
|
||||
this.originalException = originalException;
|
||||
this.location = proto.expressionAsString;
|
||||
this.message = `${this.originalException} in [${this.location}]`;
|
||||
}
|
||||
|
||||
toString():string {
|
||||
return this.message;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import {List} from 'facade/collection';
|
||||
|
||||
export class ChangeRecord {
|
||||
bindingMemento:any;
|
||||
change:any;
|
||||
|
||||
constructor(bindingMemento, change) {
|
||||
this.bindingMemento = bindingMemento;
|
||||
this.change = change;
|
||||
}
|
||||
|
||||
//REMOVE IT
|
||||
get currentValue() {
|
||||
return this.change.currentValue;
|
||||
}
|
||||
|
||||
get previousValue() {
|
||||
return this.change.previousValue;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChangeDispatcher {
|
||||
onRecordChange(groupMemento, records:List<ChangeRecord>) {}
|
||||
}
|
||||
|
||||
export class ChangeDetector {
|
||||
parent:ChangeDetector;
|
||||
|
||||
addChild(cd:ChangeDetector) {}
|
||||
removeChild(cd:ChangeDetector) {}
|
||||
remove() {}
|
||||
setContext(context:any) {}
|
||||
|
||||
detectChanges() {}
|
||||
checkNoChanges() {}
|
||||
}
|
|
@ -15,7 +15,7 @@ export class AST {
|
|||
throw new BaseException("Not supported");
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visit(visitor) {
|
||||
}
|
||||
|
||||
toString():string {
|
||||
|
@ -28,12 +28,12 @@ export class EmptyExpr extends AST {
|
|||
return null;
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visit(visitor) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export class Collection extends AST {
|
||||
export class Structural extends AST {
|
||||
value:AST;
|
||||
constructor(value:AST) {
|
||||
this.value = value;
|
||||
|
@ -43,8 +43,8 @@ export class Collection extends AST {
|
|||
return value.eval(context);
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitCollection(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitStructural(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,8 +53,8 @@ export class ImplicitReceiver extends AST {
|
|||
return context;
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitImplicitReceiver(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitImplicitReceiver(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,8 +76,8 @@ export class Chain extends AST {
|
|||
return result;
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitChain(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitChain(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,8 +99,8 @@ export class Conditional extends AST {
|
|||
}
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitConditional(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitConditional(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,8 +146,8 @@ export class AccessMember extends AST {
|
|||
return this.setter(evaluatedContext, value);
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitAccessMember(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitAccessMember(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,8 +176,8 @@ export class KeyedAccess extends AST {
|
|||
return value;
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitKeyedAccess(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitKeyedAccess(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,8 +193,8 @@ export class Formatter extends AST {
|
|||
this.allArgs = ListWrapper.concat([exp], args);
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitFormatter(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitFormatter(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,8 +208,8 @@ export class LiteralPrimitive extends AST {
|
|||
return this.value;
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitLiteralPrimitive(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitLiteralPrimitive(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,8 +223,8 @@ export class LiteralArray extends AST {
|
|||
return ListWrapper.map(this.expressions, (e) => e.eval(context));
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitLiteralArray(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitLiteralArray(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,8 +244,8 @@ export class LiteralMap extends AST {
|
|||
return res;
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitLiteralMap(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitLiteralMap(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -285,8 +285,8 @@ export class Binary extends AST {
|
|||
throw 'Internal error [$operation] not handled';
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitBinary(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitBinary(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -300,8 +300,8 @@ export class PrefixNot extends AST {
|
|||
return !this.expression.eval(context);
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitPrefixNot(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitPrefixNot(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,8 +317,8 @@ export class Assignment extends AST {
|
|||
return this.target.assign(context, this.value.eval(context));
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitAssignment(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitAssignment(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -349,8 +349,8 @@ export class MethodCall extends AST {
|
|||
return this.fn(evaluatedContext, evaluatedArgs);
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitMethodCall(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitMethodCall(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -370,8 +370,8 @@ export class FunctionCall extends AST {
|
|||
return FunctionWrapper.apply(obj, evalList(context, this.args));
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitFunctionCall(this, args);
|
||||
visit(visitor) {
|
||||
return visitor.visitFunctionCall(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -397,8 +397,8 @@ export class ASTWithSource extends AST {
|
|||
return this.ast.assign(context, value);
|
||||
}
|
||||
|
||||
visit(visitor, args) {
|
||||
return this.ast.visit(visitor, args);
|
||||
visit(visitor) {
|
||||
return this.ast.visit(visitor);
|
||||
}
|
||||
|
||||
toString():string {
|
||||
|
@ -420,21 +420,21 @@ export class TemplateBinding {
|
|||
|
||||
//INTERFACE
|
||||
export class AstVisitor {
|
||||
visitAccessMember(ast:AccessMember, args) {}
|
||||
visitAssignment(ast:Assignment, args) {}
|
||||
visitBinary(ast:Binary, args) {}
|
||||
visitChain(ast:Chain, args){}
|
||||
visitCollection(ast:Collection, args) {}
|
||||
visitConditional(ast:Conditional, args) {}
|
||||
visitFormatter(ast:Formatter, args) {}
|
||||
visitFunctionCall(ast:FunctionCall, args) {}
|
||||
visitImplicitReceiver(ast:ImplicitReceiver, args) {}
|
||||
visitKeyedAccess(ast:KeyedAccess, args) {}
|
||||
visitLiteralArray(ast:LiteralArray, args) {}
|
||||
visitLiteralMap(ast:LiteralMap, args) {}
|
||||
visitLiteralPrimitive(ast:LiteralPrimitive, args) {}
|
||||
visitMethodCall(ast:MethodCall, args) {}
|
||||
visitPrefixNot(ast:PrefixNot, args) {}
|
||||
visitAccessMember(ast:AccessMember) {}
|
||||
visitAssignment(ast:Assignment) {}
|
||||
visitBinary(ast:Binary) {}
|
||||
visitChain(ast:Chain){}
|
||||
visitStructural(ast:Structural) {}
|
||||
visitConditional(ast:Conditional) {}
|
||||
visitFormatter(ast:Formatter) {}
|
||||
visitFunctionCall(ast:FunctionCall) {}
|
||||
visitImplicitReceiver(ast:ImplicitReceiver) {}
|
||||
visitKeyedAccess(ast:KeyedAccess) {}
|
||||
visitLiteralArray(ast:LiteralArray) {}
|
||||
visitLiteralMap(ast:LiteralMap) {}
|
||||
visitLiteralPrimitive(ast:LiteralPrimitive) {}
|
||||
visitMethodCall(ast:MethodCall) {}
|
||||
visitPrefixNot(ast:PrefixNot) {}
|
||||
}
|
||||
|
||||
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]];
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
import {isPresent, isBlank, BaseException} from 'facade/lang';
|
||||
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
|
||||
|
||||
import {
|
||||
AccessMember,
|
||||
Assignment,
|
||||
AST,
|
||||
ASTWithSource,
|
||||
AstVisitor,
|
||||
Binary,
|
||||
Chain,
|
||||
Structural,
|
||||
Conditional,
|
||||
Formatter,
|
||||
FunctionCall,
|
||||
ImplicitReceiver,
|
||||
KeyedAccess,
|
||||
LiteralArray,
|
||||
LiteralMap,
|
||||
LiteralPrimitive,
|
||||
MethodCall,
|
||||
PrefixNot
|
||||
} from './parser/ast';
|
||||
|
||||
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
|
||||
import {ChangeDispatcher, ChangeDetector} from './interfaces';
|
||||
import {DynamicChangeDetector} from './dynamic_change_detector';
|
||||
|
||||
export const RECORD_TYPE_SELF = 0;
|
||||
export const RECORD_TYPE_PROPERTY = 1;
|
||||
export const RECORD_TYPE_INVOKE_METHOD = 2;
|
||||
export const RECORD_TYPE_CONST = 3;
|
||||
export const RECORD_TYPE_INVOKE_CLOSURE = 4;
|
||||
export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 5;
|
||||
export const RECORD_TYPE_INVOKE_FORMATTER = 6;
|
||||
export const RECORD_TYPE_STRUCTURAL_CHECK = 10;
|
||||
|
||||
export class ProtoRecord {
|
||||
mode:number;
|
||||
name:string;
|
||||
funcOrValue:any;
|
||||
args:List;
|
||||
contextIndex:number;
|
||||
record_type_selfIndex:number;
|
||||
bindingMemento:any;
|
||||
groupMemento:any;
|
||||
terminal:boolean;
|
||||
expressionAsString:string;
|
||||
|
||||
constructor(mode:number,
|
||||
name:string,
|
||||
funcOrValue,
|
||||
args:List,
|
||||
contextIndex:number,
|
||||
record_type_selfIndex:number,
|
||||
bindingMemento:any,
|
||||
groupMemento:any,
|
||||
terminal:boolean,
|
||||
expressionAsString:string) {
|
||||
|
||||
this.mode = mode;
|
||||
this.name = name;
|
||||
this.funcOrValue = funcOrValue;
|
||||
this.args = args;
|
||||
this.contextIndex = contextIndex;
|
||||
this.record_type_selfIndex = record_type_selfIndex;
|
||||
this.bindingMemento = bindingMemento;
|
||||
this.groupMemento = groupMemento;
|
||||
this.terminal = terminal;
|
||||
this.expressionAsString = expressionAsString;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProtoChangeDetector {
|
||||
records:List<ProtoRecord>;
|
||||
|
||||
constructor() {
|
||||
this.records = [];
|
||||
}
|
||||
|
||||
addAst(ast:AST, bindingMemento:any, groupMemento:any = null, structural:boolean = false) {
|
||||
if (structural) ast = new Structural(ast);
|
||||
|
||||
var c = new ProtoOperationsCreator(bindingMemento, groupMemento,
|
||||
this.records.length, ast.toString());
|
||||
ast.visit(c);
|
||||
|
||||
if (! ListWrapper.isEmpty(c.protoRecords)) {
|
||||
var last = ListWrapper.last(c.protoRecords);
|
||||
last.terminal = true;
|
||||
this.records = ListWrapper.concat(this.records, c.protoRecords);
|
||||
}
|
||||
}
|
||||
|
||||
instantiate(dispatcher:any, formatters:Map) {
|
||||
return new DynamicChangeDetector(dispatcher, formatters, this.records);
|
||||
}
|
||||
}
|
||||
|
||||
class ProtoOperationsCreator {
|
||||
protoRecords:List;
|
||||
bindingMemento:any;
|
||||
groupMemento:any;
|
||||
contextIndex:number;
|
||||
expressionAsString:string;
|
||||
|
||||
constructor(bindingMemento:any, groupMemento:any, contextIndex:number, expressionAsString:string) {
|
||||
this.protoRecords = [];
|
||||
this.bindingMemento = bindingMemento;
|
||||
this.groupMemento = groupMemento;
|
||||
this.contextIndex = contextIndex;
|
||||
this.expressionAsString = expressionAsString;
|
||||
}
|
||||
|
||||
visitImplicitReceiver(ast:ImplicitReceiver) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
visitLiteralPrimitive(ast:LiteralPrimitive) {
|
||||
return this._addRecord(RECORD_TYPE_CONST, null, ast.value, [], 0);
|
||||
}
|
||||
|
||||
visitAccessMember(ast:AccessMember) {
|
||||
var receiver = ast.receiver.visit(this);
|
||||
return this._addRecord(RECORD_TYPE_PROPERTY, ast.name, ast.getter, [], receiver);
|
||||
}
|
||||
|
||||
visitFormatter(ast:Formatter) {
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.name, this._visitAll(ast.allArgs), 0);
|
||||
}
|
||||
|
||||
visitMethodCall(ast:MethodCall) {
|
||||
var receiver = ast.receiver.visit(this);
|
||||
var args = this._visitAll(ast.args);
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_METHOD, ast.name, ast.fn, args, receiver);
|
||||
}
|
||||
|
||||
visitFunctionCall(ast:FunctionCall) {
|
||||
var target = ast.target.visit(this);
|
||||
var args = this._visitAll(ast.args);
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_CLOSURE, null, null, args, target);
|
||||
}
|
||||
|
||||
visitLiteralArray(ast:LiteralArray) {
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Array()", _arrayFn(ast.expressions.length),
|
||||
this._visitAll(ast.expressions), 0);
|
||||
}
|
||||
|
||||
visitLiteralMap(ast:LiteralMap) {
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Map()", _mapFn(ast.keys, ast.values.length),
|
||||
this._visitAll(ast.values), 0);
|
||||
}
|
||||
|
||||
visitBinary(ast:Binary) {
|
||||
var left = ast.left.visit(this);
|
||||
var right = ast.right.visit(this);
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, ast.operation, _operationToFunction(ast.operation), [left, right], 0);
|
||||
}
|
||||
|
||||
visitPrefixNot(ast:PrefixNot) {
|
||||
var exp = ast.expression.visit(this)
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "-", _operation_negate, [exp], 0);
|
||||
}
|
||||
|
||||
visitConditional(ast:Conditional) {
|
||||
var c = ast.condition.visit(this);
|
||||
var t = ast.trueExp.visit(this);
|
||||
var f = ast.falseExp.visit(this);
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "?:", _cond, [c,t,f], 0);
|
||||
}
|
||||
|
||||
visitStructural(ast:Structural) {
|
||||
var value = ast.value.visit(this);
|
||||
return this._addRecord(RECORD_TYPE_STRUCTURAL_CHECK, "record_type_structural_check", null, [], value);
|
||||
}
|
||||
|
||||
visitKeyedAccess(ast:KeyedAccess) {
|
||||
var obj = ast.obj.visit(this);
|
||||
var key = ast.key.visit(this);
|
||||
return this._addRecord(RECORD_TYPE_INVOKE_METHOD, "[]", _keyedAccess, [key], obj);
|
||||
}
|
||||
|
||||
_visitAll(asts:List) {
|
||||
var res = ListWrapper.createFixedSize(asts.length);
|
||||
for (var i = 0; i < asts.length; ++i) {
|
||||
res[i] = asts[i].visit(this);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
_addRecord(type, name, funcOrValue, args, context) {
|
||||
var record_type_selfIndex = ++ this.contextIndex;
|
||||
ListWrapper.push(this.protoRecords,
|
||||
new ProtoRecord(type, name, funcOrValue, args, context, record_type_selfIndex,
|
||||
this.bindingMemento, this.groupMemento, false, this.expressionAsString));
|
||||
return record_type_selfIndex;
|
||||
}
|
||||
}
|
||||
|
||||
function _arrayFn(length:int) {
|
||||
switch (length) {
|
||||
case 0: return () => [];
|
||||
case 1: return (a1) => [a1];
|
||||
case 2: return (a1, a2) => [a1, a2];
|
||||
case 3: return (a1, a2, a3) => [a1, a2, a3];
|
||||
case 4: return (a1, a2, a3, a4) => [a1, a2, a3, a4];
|
||||
case 5: return (a1, a2, a3, a4, a5) => [a1, a2, a3, a4, a5];
|
||||
case 6: return (a1, a2, a3, a4, a5, a6) => [a1, a2, a3, a4, a5, a6];
|
||||
case 7: return (a1, a2, a3, a4, a5, a6, a7) => [a1, a2, a3, a4, a5, a6, a7];
|
||||
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => [a1, a2, a3, a4, a5, a6, a7, a8];
|
||||
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => [a1, a2, a3, a4, a5, a6, a7, a8, a9];
|
||||
default: throw new BaseException(`Does not support literal arrays with more than 9 elements`);
|
||||
}
|
||||
}
|
||||
|
||||
function _mapFn(keys:List, length:int) {
|
||||
function buildMap(values) {
|
||||
var res = StringMapWrapper.create();
|
||||
for(var i = 0; i < keys.length; ++i) {
|
||||
StringMapWrapper.set(res, keys[i], values[i]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
switch (length) {
|
||||
case 0: return () => [];
|
||||
case 1: return (a1) => buildMap([a1]);
|
||||
case 2: return (a1, a2) => buildMap([a1, a2]);
|
||||
case 3: return (a1, a2, a3) => buildMap([a1, a2, a3]);
|
||||
case 4: return (a1, a2, a3, a4) => buildMap([a1, a2, a3, a4]);
|
||||
case 5: return (a1, a2, a3, a4, a5) => buildMap([a1, a2, a3, a4, a5]);
|
||||
case 6: return (a1, a2, a3, a4, a5, a6) => buildMap([a1, a2, a3, a4, a5, a6]);
|
||||
case 7: return (a1, a2, a3, a4, a5, a6, a7) => buildMap([a1, a2, a3, a4, a5, a6, a7]);
|
||||
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8]);
|
||||
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8, a9]);
|
||||
default: throw new BaseException(`Does not support literal maps with more than 9 elements`);
|
||||
}
|
||||
}
|
||||
|
||||
function _operationToFunction(operation:string):Function {
|
||||
switch(operation) {
|
||||
case '+' : return _operation_add;
|
||||
case '-' : return _operation_subtract;
|
||||
case '*' : return _operation_multiply;
|
||||
case '/' : return _operation_divide;
|
||||
case '%' : return _operation_remainder;
|
||||
case '==' : return _operation_equals;
|
||||
case '!=' : return _operation_not_equals;
|
||||
case '<' : return _operation_less_then;
|
||||
case '>' : return _operation_greater_then;
|
||||
case '<=' : return _operation_less_or_equals_then;
|
||||
case '>=' : return _operation_greater_or_equals_then;
|
||||
case '&&' : return _operation_logical_and;
|
||||
case '||' : return _operation_logical_or;
|
||||
default: throw new BaseException(`Unsupported operation ${operation}`);
|
||||
}
|
||||
}
|
||||
|
||||
function _operation_negate(value) {return !value;}
|
||||
function _operation_add(left, right) {return left + right;}
|
||||
function _operation_subtract(left, right) {return left - right;}
|
||||
function _operation_multiply(left, right) {return left * right;}
|
||||
function _operation_divide(left, right) {return left / right;}
|
||||
function _operation_remainder(left, right) {return left % right;}
|
||||
function _operation_equals(left, right) {return left == right;}
|
||||
function _operation_not_equals(left, right) {return left != right;}
|
||||
function _operation_less_then(left, right) {return left < right;}
|
||||
function _operation_greater_then(left, right) {return left > right;}
|
||||
function _operation_less_or_equals_then(left, right) {return left <= right;}
|
||||
function _operation_greater_or_equals_then(left, right) {return left >= right;}
|
||||
function _operation_logical_and(left, right) {return left && right;}
|
||||
function _operation_logical_or(left, right) {return left || right;}
|
||||
function _cond(cond, trueVal, falseVal) {return cond ? trueVal : falseVal;}
|
||||
|
||||
function _keyedAccess(obj, args) {
|
||||
return obj[args[0]];
|
||||
}
|
|
@ -1,505 +0,0 @@
|
|||
import {ProtoRecordRange, RecordRange} from './record_range';
|
||||
import {FIELD, isPresent, isBlank, int, StringWrapper, FunctionWrapper, BaseException} from 'facade/lang';
|
||||
import {List, Map, ListWrapper, MapWrapper} from 'facade/collection';
|
||||
import {ArrayChanges} from './array_changes';
|
||||
import {KeyValueChanges} from './keyvalue_changes';
|
||||
|
||||
var _fresh = new Object();
|
||||
|
||||
const RECORD_TYPE_MASK = 0x000f;
|
||||
export const RECORD_TYPE_CONST = 0x0000;
|
||||
export const RECORD_TYPE_INVOKE_CLOSURE = 0x0001;
|
||||
export const RECORD_TYPE_INVOKE_FORMATTER = 0x0002;
|
||||
export const RECORD_TYPE_INVOKE_METHOD = 0x0003;
|
||||
export const RECORD_TYPE_INVOKE_PURE_FUNCTION = 0x0004;
|
||||
const RECORD_TYPE_ARRAY = 0x0005;
|
||||
const RECORD_TYPE_KEY_VALUE = 0x0006;
|
||||
const RECORD_TYPE_MARKER = 0x0007;
|
||||
export const RECORD_TYPE_PROPERTY = 0x0008;
|
||||
const RECORD_TYPE_NULL= 0x0009;
|
||||
|
||||
const RECORD_FLAG_DISABLED = 0x0100;
|
||||
export const RECORD_FLAG_IMPLICIT_RECEIVER = 0x0200;
|
||||
export const RECORD_FLAG_COLLECTION = 0x0400;
|
||||
|
||||
/**
|
||||
* For now we are dropping expression coalescence. We can always add it later, but
|
||||
* real world numbers show that it does not provide significant benefits.
|
||||
*/
|
||||
export class ProtoRecord {
|
||||
recordRange:ProtoRecordRange;
|
||||
_mode:int;
|
||||
context:any;
|
||||
funcOrValue:any;
|
||||
arity:int;
|
||||
name:string;
|
||||
dest:any;
|
||||
groupMemento:any;
|
||||
expressionAsString:string;
|
||||
|
||||
next:ProtoRecord;
|
||||
|
||||
recordInConstruction:Record;
|
||||
|
||||
constructor(recordRange:ProtoRecordRange,
|
||||
mode:int,
|
||||
funcOrValue,
|
||||
arity:int,
|
||||
name:string,
|
||||
dest,
|
||||
groupMemento,
|
||||
expressionAsString:string) {
|
||||
|
||||
this.recordRange = recordRange;
|
||||
this._mode = mode;
|
||||
this.funcOrValue = funcOrValue;
|
||||
this.arity = arity;
|
||||
this.name = name;
|
||||
this.dest = dest;
|
||||
this.groupMemento = groupMemento;
|
||||
this.expressionAsString = expressionAsString;
|
||||
|
||||
this.next = null;
|
||||
// The concrete Record instantiated from this ProtoRecord
|
||||
this.recordInConstruction = null;
|
||||
}
|
||||
|
||||
setIsImplicitReceiver() {
|
||||
this._mode |= RECORD_FLAG_IMPLICIT_RECEIVER;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a Record for keeping track of changes. A change is a difference between previous
|
||||
* and current value.
|
||||
*
|
||||
* By default changes are detected using dirty checking, but a notifier can be present which can
|
||||
* notify the records of changes by means other than dirty checking. For example Object.observe
|
||||
* or events on DOM elements.
|
||||
*
|
||||
* DESIGN NOTES:
|
||||
* - No inheritance allowed so that code is monomorphic for performance.
|
||||
* - Atomic watch operations
|
||||
* - Defaults to dirty checking
|
||||
* - Keep this object as lean as possible. (Lean in number of fields)
|
||||
*/
|
||||
export class Record {
|
||||
recordRange:RecordRange;
|
||||
protoRecord:ProtoRecord;
|
||||
next:Record;
|
||||
prev:Record;
|
||||
|
||||
/// This reference can change.
|
||||
nextEnabled:Record;
|
||||
|
||||
/// This reference can change.
|
||||
prevEnabled:Record;
|
||||
|
||||
previousValue;
|
||||
currentValue;
|
||||
|
||||
_mode:int;
|
||||
context;
|
||||
funcOrValue;
|
||||
args:List;
|
||||
|
||||
// Opaque data which will be the target of notification.
|
||||
// If the object is instance of Record, then it it is directly processed
|
||||
// Otherwise it is the context used by ChangeDispatcher.
|
||||
dest;
|
||||
|
||||
constructor(recordRange:RecordRange, protoRecord:ProtoRecord, formatters:Map) {
|
||||
this.recordRange = recordRange;
|
||||
this.protoRecord = protoRecord;
|
||||
|
||||
this.next = null;
|
||||
this.prev = null;
|
||||
this.nextEnabled = null;
|
||||
this.prevEnabled = null;
|
||||
this.dest = null;
|
||||
|
||||
this.previousValue = null;
|
||||
|
||||
this.context = null;
|
||||
this.funcOrValue = null;
|
||||
this.args = null;
|
||||
|
||||
if (isBlank(protoRecord)) {
|
||||
this._mode = RECORD_TYPE_MARKER | RECORD_FLAG_DISABLED;
|
||||
return;
|
||||
}
|
||||
|
||||
this._mode = protoRecord._mode;
|
||||
|
||||
// Return early for collections, further init delayed until updateContext()
|
||||
if (this.isCollection()) return;
|
||||
|
||||
this.currentValue = _fresh;
|
||||
|
||||
var type = this.getType();
|
||||
|
||||
if (type === RECORD_TYPE_CONST) {
|
||||
this.funcOrValue = protoRecord.funcOrValue;
|
||||
|
||||
} else if (type === RECORD_TYPE_INVOKE_PURE_FUNCTION) {
|
||||
this.funcOrValue = protoRecord.funcOrValue;
|
||||
this.args = ListWrapper.createFixedSize(protoRecord.arity);
|
||||
|
||||
} else if (type === RECORD_TYPE_INVOKE_FORMATTER) {
|
||||
this.funcOrValue = MapWrapper.get(formatters, protoRecord.funcOrValue);
|
||||
this.args = ListWrapper.createFixedSize(protoRecord.arity);
|
||||
|
||||
} else if (type === RECORD_TYPE_INVOKE_METHOD) {
|
||||
this.funcOrValue = protoRecord.funcOrValue;
|
||||
this.args = ListWrapper.createFixedSize(protoRecord.arity);
|
||||
|
||||
} else if (type === RECORD_TYPE_INVOKE_CLOSURE) {
|
||||
this.args = ListWrapper.createFixedSize(protoRecord.arity);
|
||||
|
||||
} else if (type === RECORD_TYPE_PROPERTY) {
|
||||
this.funcOrValue = protoRecord.funcOrValue;
|
||||
}
|
||||
}
|
||||
|
||||
// getters & setters perform much worse on some browsers
|
||||
// see http://jsperf.com/vicb-getter-vs-function
|
||||
getType():int {
|
||||
return this._mode & RECORD_TYPE_MASK;
|
||||
}
|
||||
|
||||
setType(value:int) {
|
||||
this._mode = (this._mode & ~RECORD_TYPE_MASK) | value;
|
||||
}
|
||||
|
||||
isDisabled():boolean {
|
||||
return (this._mode & RECORD_FLAG_DISABLED) === RECORD_FLAG_DISABLED;
|
||||
}
|
||||
|
||||
isEnabled():boolean {
|
||||
return !this.isDisabled();
|
||||
}
|
||||
|
||||
_setDisabled(value:boolean) {
|
||||
if (value) {
|
||||
this._mode |= RECORD_FLAG_DISABLED;
|
||||
} else {
|
||||
this._mode &= ~RECORD_FLAG_DISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (this.isEnabled()) return;
|
||||
|
||||
var prevEnabled = this.findPrevEnabled();
|
||||
var nextEnabled = this.findNextEnabled();
|
||||
|
||||
this.prevEnabled = prevEnabled;
|
||||
this.nextEnabled = nextEnabled;
|
||||
|
||||
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = this;
|
||||
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = this;
|
||||
|
||||
this._setDisabled(false);
|
||||
}
|
||||
|
||||
disable() {
|
||||
var prevEnabled = this.prevEnabled;
|
||||
var nextEnabled = this.nextEnabled;
|
||||
|
||||
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled;
|
||||
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled;
|
||||
|
||||
this._setDisabled(true);
|
||||
}
|
||||
|
||||
isImplicitReceiver():boolean {
|
||||
return (this._mode & RECORD_FLAG_IMPLICIT_RECEIVER) === RECORD_FLAG_IMPLICIT_RECEIVER;
|
||||
}
|
||||
|
||||
isCollection():boolean {
|
||||
return (this._mode & RECORD_FLAG_COLLECTION) === RECORD_FLAG_COLLECTION;
|
||||
}
|
||||
|
||||
static createMarker(rr:RecordRange):Record {
|
||||
return new Record(rr, null, null);
|
||||
}
|
||||
|
||||
check():boolean {
|
||||
if (this.isCollection()) {
|
||||
return this._checkCollection();
|
||||
} else {
|
||||
return this._checkSingleRecord();
|
||||
}
|
||||
}
|
||||
|
||||
_checkSingleRecord():boolean {
|
||||
this.previousValue = this.currentValue;
|
||||
this.currentValue = this._calculateNewValue();
|
||||
if (isSame(this.previousValue, this.currentValue)) return false;
|
||||
this._updateDestination();
|
||||
return true;
|
||||
}
|
||||
|
||||
_updateDestination() {
|
||||
if (this.dest instanceof Record) {
|
||||
if (isPresent(this.protoRecord.dest.position)) {
|
||||
this.dest.updateArg(this.currentValue, this.protoRecord.dest.position);
|
||||
} else {
|
||||
this.dest.updateContext(this.currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return whether the content has changed
|
||||
_checkCollection():boolean {
|
||||
switch(this.getType()) {
|
||||
case RECORD_TYPE_KEY_VALUE:
|
||||
var kvChangeDetector:KeyValueChanges = this.currentValue;
|
||||
return kvChangeDetector.check(this.context);
|
||||
|
||||
case RECORD_TYPE_ARRAY:
|
||||
var arrayChangeDetector:ArrayChanges = this.currentValue;
|
||||
return arrayChangeDetector.check(this.context);
|
||||
|
||||
case RECORD_TYPE_NULL:
|
||||
// no need to check the content again unless the context changes
|
||||
this.disable();
|
||||
this.currentValue = null;
|
||||
return true;
|
||||
|
||||
default:
|
||||
throw new BaseException(`Unsupported record type (${this.getType()})`);
|
||||
}
|
||||
}
|
||||
|
||||
_calculateNewValue() {
|
||||
try {
|
||||
return this.__calculateNewValue();
|
||||
} catch (e) {
|
||||
throw new ChangeDetectionError(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
__calculateNewValue() {
|
||||
switch (this.getType()) {
|
||||
case RECORD_TYPE_PROPERTY:
|
||||
var propertyGetter:Function = this.funcOrValue;
|
||||
return propertyGetter(this.context);
|
||||
|
||||
case RECORD_TYPE_INVOKE_METHOD:
|
||||
var methodInvoker:Function = this.funcOrValue;
|
||||
return methodInvoker(this.context, this.args);
|
||||
|
||||
case RECORD_TYPE_INVOKE_CLOSURE:
|
||||
return FunctionWrapper.apply(this.context, this.args);
|
||||
|
||||
case RECORD_TYPE_INVOKE_PURE_FUNCTION:
|
||||
case RECORD_TYPE_INVOKE_FORMATTER:
|
||||
this.disable();
|
||||
return FunctionWrapper.apply(this.funcOrValue, this.args);
|
||||
|
||||
case RECORD_TYPE_CONST:
|
||||
this.disable();
|
||||
return this.funcOrValue;
|
||||
|
||||
default:
|
||||
throw new BaseException(`Unsupported record type (${this.getType()})`);
|
||||
}
|
||||
}
|
||||
|
||||
updateArg(value, position:int) {
|
||||
this.args[position] = value;
|
||||
this.enable();
|
||||
}
|
||||
|
||||
updateContext(value) {
|
||||
this.context = value;
|
||||
this.enable();
|
||||
|
||||
if (this.isCollection()) {
|
||||
if (ArrayChanges.supports(value)) {
|
||||
if (this.getType() != RECORD_TYPE_ARRAY) {
|
||||
this.setType(RECORD_TYPE_ARRAY);
|
||||
this.currentValue = new ArrayChanges();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (KeyValueChanges.supports(value)) {
|
||||
if (this.getType() != RECORD_TYPE_KEY_VALUE) {
|
||||
this.setType(RECORD_TYPE_KEY_VALUE);
|
||||
this.currentValue = new KeyValueChanges();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBlank(value)) {
|
||||
this.setType(RECORD_TYPE_NULL);
|
||||
} else {
|
||||
throw new BaseException("Collection records must be array like, map like or null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
terminatesExpression():boolean {
|
||||
return !(this.dest instanceof Record);
|
||||
}
|
||||
|
||||
isMarkerRecord():boolean {
|
||||
return this.getType() == RECORD_TYPE_MARKER;
|
||||
}
|
||||
|
||||
expressionMemento() {
|
||||
return this.protoRecord.dest;
|
||||
}
|
||||
|
||||
expressionAsString() {
|
||||
return this.protoRecord.expressionAsString;
|
||||
}
|
||||
|
||||
groupMemento() {
|
||||
return isPresent(this.protoRecord) ? this.protoRecord.groupMemento : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the next enabled record. This search is not limited to the current range.
|
||||
*
|
||||
* [H ER1 T] [H ER2 T] _nextEnable(ER1) will return ER2
|
||||
*
|
||||
* The function skips disabled ranges.
|
||||
*/
|
||||
findNextEnabled() {
|
||||
if (this.isEnabled()) return this.nextEnabled;
|
||||
|
||||
var record = this.next;
|
||||
while (isPresent(record) && record.isDisabled()) {
|
||||
if (record.isMarkerRecord() && record.recordRange.disabled) {
|
||||
record = record.recordRange.tailRecord.next;
|
||||
} else {
|
||||
record = record.next;
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the prev enabled record. This search is not limited to the current range.
|
||||
*
|
||||
* [H ER1 T] [H ER2 T] _nextEnable(ER2) will return ER1
|
||||
*
|
||||
* The function skips disabled ranges.
|
||||
*/
|
||||
findPrevEnabled() {
|
||||
if (this.isEnabled()) return this.prevEnabled;
|
||||
|
||||
var record = this.prev;
|
||||
while (isPresent(record) && record.isDisabled()) {
|
||||
if (record.isMarkerRecord() && record.recordRange.disabled) {
|
||||
record = record.recordRange.headRecord.prev;
|
||||
} else {
|
||||
record = record.prev;
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
inspect() {
|
||||
return _inspect(this);
|
||||
}
|
||||
|
||||
inspectRange() {
|
||||
return this.recordRange.inspect();
|
||||
}
|
||||
}
|
||||
|
||||
function _inspect(record:Record) {
|
||||
function mode() {
|
||||
switch (record.getType()) {
|
||||
case RECORD_TYPE_PROPERTY:
|
||||
return "property";
|
||||
case RECORD_TYPE_INVOKE_METHOD:
|
||||
return "invoke_method";
|
||||
case RECORD_TYPE_INVOKE_CLOSURE:
|
||||
return "invoke_closure";
|
||||
case RECORD_TYPE_INVOKE_PURE_FUNCTION:
|
||||
return "pure_function";
|
||||
case RECORD_TYPE_INVOKE_FORMATTER:
|
||||
return "invoke_formatter";
|
||||
case RECORD_TYPE_CONST:
|
||||
return "const";
|
||||
case RECORD_TYPE_KEY_VALUE:
|
||||
return "key_value";
|
||||
case RECORD_TYPE_ARRAY:
|
||||
return "array";
|
||||
case RECORD_TYPE_NULL:
|
||||
return "null";
|
||||
case RECORD_TYPE_MARKER:
|
||||
return "marker";
|
||||
default:
|
||||
return "unexpected type!";
|
||||
}
|
||||
}
|
||||
|
||||
function disabled() {
|
||||
return record.isDisabled() ? "disabled" : "enabled";
|
||||
}
|
||||
|
||||
function description() {
|
||||
var name = isPresent(record.protoRecord) ? record.protoRecord.name : "";
|
||||
var exp = isPresent(record.protoRecord) ? record.protoRecord.expressionAsString : "";
|
||||
var currValue = record.currentValue;
|
||||
var context = record.context;
|
||||
|
||||
return `${mode()}, ${name}, ${disabled()} ` +
|
||||
` Current: ${currValue}, Context: ${context} in [${exp}]`;
|
||||
}
|
||||
|
||||
if (isBlank(record)) return null;
|
||||
if (!(record instanceof Record)) return record;
|
||||
|
||||
return new _RecordInspect(description(), record);
|
||||
}
|
||||
|
||||
class _RecordInspect {
|
||||
description:string;
|
||||
record:Record;
|
||||
|
||||
constructor(description:string,record:Record) {
|
||||
this.description = description;
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
get next() {
|
||||
return _inspect(this.record.next);
|
||||
}
|
||||
get nextEnabled() {
|
||||
return _inspect(this.record.nextEnabled);
|
||||
}
|
||||
get dest() {
|
||||
return _inspect(this.record.dest);
|
||||
}
|
||||
}
|
||||
|
||||
function isSame(a, b) {
|
||||
if (a === b) return true;
|
||||
if ((a !== a) && (b !== b)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export class ChangeDetectionError extends Error {
|
||||
message:string;
|
||||
originalException:any;
|
||||
location:string;
|
||||
|
||||
constructor(record:Record, originalException:any) {
|
||||
this.originalException = originalException;
|
||||
this.location = record.protoRecord.expressionAsString;
|
||||
this.message = `${this.originalException} in [${this.location}]`;
|
||||
}
|
||||
|
||||
toString():string {
|
||||
return this.message;
|
||||
}
|
||||
}
|
|
@ -1,558 +0,0 @@
|
|||
import {
|
||||
ProtoRecord,
|
||||
Record,
|
||||
RECORD_FLAG_COLLECTION,
|
||||
RECORD_FLAG_IMPLICIT_RECEIVER,
|
||||
RECORD_TYPE_CONST,
|
||||
RECORD_TYPE_INVOKE_CLOSURE,
|
||||
RECORD_TYPE_INVOKE_FORMATTER,
|
||||
RECORD_TYPE_INVOKE_METHOD,
|
||||
RECORD_TYPE_INVOKE_PURE_FUNCTION,
|
||||
RECORD_TYPE_PROPERTY
|
||||
} from './record';
|
||||
|
||||
import {FIELD, IMPLEMENTS, isBlank, isPresent, int, autoConvertAdd, BaseException,
|
||||
NumberWrapper} from 'facade/lang';
|
||||
import {List, Map, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
|
||||
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
|
||||
import {
|
||||
AccessMember,
|
||||
Assignment,
|
||||
AST,
|
||||
AstVisitor,
|
||||
Binary,
|
||||
Chain,
|
||||
Collection,
|
||||
Conditional,
|
||||
Formatter,
|
||||
FunctionCall,
|
||||
ImplicitReceiver,
|
||||
KeyedAccess,
|
||||
LiteralArray,
|
||||
LiteralMap,
|
||||
LiteralPrimitive,
|
||||
MethodCall,
|
||||
PrefixNot
|
||||
} from './parser/ast';
|
||||
|
||||
export class ProtoRecordRange {
|
||||
recordCreator: ProtoRecordCreator;
|
||||
constructor() {
|
||||
this.recordCreator = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses [ast] into [ProtoRecord]s and adds them to [ProtoRecordRange].
|
||||
*
|
||||
* @param astWithSource The expression to watch
|
||||
* @param expressionMemento an opaque object which will be passed to ChangeDispatcher on
|
||||
* detecting a change.
|
||||
* @param groupMemento
|
||||
* @param content Whether to watch collection content (true) or reference (false, default)
|
||||
*/
|
||||
addRecordsFromAST(ast:AST,
|
||||
expressionMemento,
|
||||
groupMemento,
|
||||
content:boolean = false)
|
||||
{
|
||||
if (this.recordCreator === null) {
|
||||
this.recordCreator = new ProtoRecordCreator(this);
|
||||
}
|
||||
|
||||
if (content) {
|
||||
ast = new Collection(ast);
|
||||
}
|
||||
this.recordCreator.createRecordsFromAST(ast, expressionMemento, groupMemento);
|
||||
}
|
||||
|
||||
// TODO(rado): the type annotation should be dispatcher:ChangeDispatcher.
|
||||
// but @Implements is not ready yet.
|
||||
instantiate(dispatcher, formatters:Map):RecordRange {
|
||||
var recordRange:RecordRange = new RecordRange(this, dispatcher);
|
||||
if (this.recordCreator !== null) {
|
||||
this._createRecords(recordRange, formatters);
|
||||
this._setDestination();
|
||||
}
|
||||
return recordRange;
|
||||
}
|
||||
|
||||
_createRecords(recordRange:RecordRange, formatters:Map) {
|
||||
for (var proto = this.recordCreator.headRecord; proto != null; proto = proto.next) {
|
||||
var record = new Record(recordRange, proto, formatters);
|
||||
proto.recordInConstruction = record;
|
||||
recordRange.addRecord(record);
|
||||
}
|
||||
}
|
||||
|
||||
_setDestination() {
|
||||
for (var proto = this.recordCreator.headRecord; proto != null; proto = proto.next) {
|
||||
if (proto.dest instanceof Destination) {
|
||||
proto.recordInConstruction.dest = proto.dest.record.recordInConstruction;
|
||||
} else {
|
||||
proto.recordInConstruction.dest = proto.dest;
|
||||
}
|
||||
proto.recordInConstruction = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RecordRange {
|
||||
protoRecordRange:ProtoRecordRange;
|
||||
dispatcher:any; //ChangeDispatcher
|
||||
headRecord:Record;
|
||||
tailRecord:Record;
|
||||
disabled:boolean;
|
||||
// TODO(rado): the type annotation should be dispatcher:ChangeDispatcher.
|
||||
// but @Implements is not ready yet.
|
||||
constructor(protoRecordRange:ProtoRecordRange, dispatcher) {
|
||||
this.protoRecordRange = protoRecordRange;
|
||||
this.dispatcher = dispatcher;
|
||||
|
||||
this.disabled = false;
|
||||
|
||||
this.headRecord = Record.createMarker(this);
|
||||
this.tailRecord = Record.createMarker(this);
|
||||
|
||||
_link(this.headRecord, this.tailRecord);
|
||||
}
|
||||
|
||||
/// addRecord assumes that the record is newly created, so it is enabled.
|
||||
addRecord(record:Record) {
|
||||
var lastRecord = this.tailRecord.prev;
|
||||
|
||||
_link(lastRecord, record);
|
||||
if (!lastRecord.isDisabled()) {
|
||||
_linkEnabled(lastRecord, record);
|
||||
}
|
||||
_link(record, this.tailRecord);
|
||||
}
|
||||
|
||||
addRange(child:RecordRange) {
|
||||
var lastRecord = this.tailRecord.prev;
|
||||
var prevEnabledRecord = this.tailRecord.findPrevEnabled();
|
||||
var nextEnabledRerord = this.tailRecord.findNextEnabled();
|
||||
|
||||
var firstEnabledChildRecord = child.findFirstEnabledRecord();
|
||||
var lastEnabledChildRecord = child.findLastEnabledRecord();
|
||||
|
||||
_link(lastRecord, child.headRecord);
|
||||
_link(child.tailRecord, this.tailRecord);
|
||||
|
||||
if (isPresent(prevEnabledRecord) && isPresent(firstEnabledChildRecord)) {
|
||||
_linkEnabled(prevEnabledRecord, firstEnabledChildRecord);
|
||||
}
|
||||
|
||||
if (isPresent(nextEnabledRerord) && isPresent(lastEnabledChildRecord)) {
|
||||
_linkEnabled(lastEnabledChildRecord, nextEnabledRerord);
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
var firstEnabledChildRecord = this.findFirstEnabledRecord();
|
||||
var next = this.tailRecord.next;
|
||||
var prev = this.headRecord.prev;
|
||||
|
||||
_link(prev, next);
|
||||
|
||||
if (isPresent(firstEnabledChildRecord)) {
|
||||
var lastEnabledChildRecord = this.findLastEnabledRecord();
|
||||
var nextEnabled = lastEnabledChildRecord.nextEnabled;
|
||||
var prevEnabled = firstEnabledChildRecord.prevEnabled;
|
||||
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled;
|
||||
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
disable() {
|
||||
var firstEnabledChildRecord = this.findFirstEnabledRecord();
|
||||
if (isPresent(firstEnabledChildRecord)) {
|
||||
// There could be a last enabled record only if first enabled exists
|
||||
var lastEnabledChildRecord = this.findLastEnabledRecord();
|
||||
var nextEnabled = lastEnabledChildRecord.nextEnabled;
|
||||
var prevEnabled = firstEnabledChildRecord.prevEnabled;
|
||||
if (isPresent(nextEnabled)) nextEnabled.prevEnabled = prevEnabled;
|
||||
if (isPresent(prevEnabled)) prevEnabled.nextEnabled = nextEnabled;
|
||||
}
|
||||
|
||||
this.disabled = true;
|
||||
}
|
||||
|
||||
enable() {
|
||||
var prevEnabledRecord = this.headRecord.findPrevEnabled();
|
||||
var nextEnabledRecord = this.tailRecord.findNextEnabled();
|
||||
|
||||
var firstEnabledthisRecord = this.findFirstEnabledRecord();
|
||||
var lastEnabledthisRecord = this.findLastEnabledRecord();
|
||||
|
||||
if (isPresent(firstEnabledthisRecord) && isPresent(prevEnabledRecord)){
|
||||
_linkEnabled(prevEnabledRecord, firstEnabledthisRecord);
|
||||
}
|
||||
|
||||
if (isPresent(lastEnabledthisRecord) && isPresent(nextEnabledRecord)){
|
||||
_linkEnabled(lastEnabledthisRecord, nextEnabledRecord);
|
||||
}
|
||||
|
||||
this.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first enabled record in the current range.
|
||||
*
|
||||
* [H ER1 ER2 R3 T] returns ER1
|
||||
* [H R1 ER2 R3 T] returns ER2
|
||||
*
|
||||
* If no enabled records, returns null.
|
||||
*
|
||||
* [H R1 R2 R3 T] returns null
|
||||
*
|
||||
* The function skips disabled sub ranges.
|
||||
*/
|
||||
findFirstEnabledRecord() {
|
||||
var record = this.headRecord.next;
|
||||
while (record !== this.tailRecord && record.isDisabled()) {
|
||||
if (record.isMarkerRecord() && record.recordRange.disabled) {
|
||||
record = record.recordRange.tailRecord.next;
|
||||
} else {
|
||||
record = record.next;
|
||||
}
|
||||
}
|
||||
return record === this.tailRecord ? null : record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last enabled record in the current range.
|
||||
*
|
||||
* [H ER1 ER2 R3 T] returns ER2
|
||||
* [H R1 ER2 R3 T] returns ER2
|
||||
*
|
||||
* If no enabled records, returns null.
|
||||
*
|
||||
* [H R1 R2 R3 T] returns null
|
||||
*
|
||||
* The function skips disabled sub ranges.
|
||||
*/
|
||||
findLastEnabledRecord() {
|
||||
var record = this.tailRecord.prev;
|
||||
while (record !== this.headRecord && record.isDisabled()) {
|
||||
if (record.isMarkerRecord() && record.recordRange.disabled) {
|
||||
record = record.recordRange.headRecord.prev;
|
||||
} else {
|
||||
record = record.prev;
|
||||
}
|
||||
}
|
||||
return record === this.headRecord ? null : record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the context (the object) on which the change detection expressions will
|
||||
* dereference themselves on. Since the RecordRange can be reused the context
|
||||
* can be re-set many times during the lifetime of the RecordRange.
|
||||
*
|
||||
* @param context the new context for change detection for the current RecordRange
|
||||
*/
|
||||
setContext(context) {
|
||||
for (var record:Record = this.headRecord;
|
||||
record != null;
|
||||
record = record.next) {
|
||||
|
||||
if (record.isImplicitReceiver()) {
|
||||
this._setContextForRecord(context, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_setContextForRecord(context, record:Record) {
|
||||
var proto = record.protoRecord;
|
||||
|
||||
while (context instanceof ContextWithVariableBindings) {
|
||||
if (context.hasBinding(proto.name)) {
|
||||
this._setVarBindingGetter(context, record, proto);
|
||||
return;
|
||||
}
|
||||
context = context.parent;
|
||||
}
|
||||
|
||||
this._setRegularGetter(context, record, proto);
|
||||
}
|
||||
|
||||
_setVarBindingGetter(context, record:Record, proto:ProtoRecord) {
|
||||
record.funcOrValue = _mapGetter(proto.name);
|
||||
record.updateContext(context.varBindings);
|
||||
}
|
||||
|
||||
_setRegularGetter(context, record:Record, proto:ProtoRecord) {
|
||||
record.funcOrValue = proto.funcOrValue;
|
||||
record.updateContext(context);
|
||||
}
|
||||
|
||||
inspect() {
|
||||
return _inspect(this);
|
||||
}
|
||||
}
|
||||
|
||||
function _inspect(recordRange:RecordRange) {
|
||||
var res = [];
|
||||
for(var r = recordRange.headRecord.next; r != recordRange.tailRecord; r = r.next){
|
||||
ListWrapper.push(res, r.inspect().description);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function _link(a:Record, b:Record) {
|
||||
a.next = b;
|
||||
b.prev = a;
|
||||
}
|
||||
|
||||
function _linkEnabled(a:Record, b:Record) {
|
||||
a.nextEnabled = b;
|
||||
b.prevEnabled = a;
|
||||
}
|
||||
|
||||
export class ChangeDispatcher {
|
||||
onRecordChange(groupMemento, records:List<Record>) {}
|
||||
}
|
||||
|
||||
//todo: vsavkin: Create Array and Context destinations?
|
||||
class Destination {
|
||||
record:ProtoRecord;
|
||||
position:int;
|
||||
constructor(record:ProtoRecord, position:int) {
|
||||
this.record = record;
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IMPLEMENTS(AstVisitor)
|
||||
class ProtoRecordCreator {
|
||||
protoRecordRange:ProtoRecordRange;
|
||||
headRecord:ProtoRecord;
|
||||
tailRecord:ProtoRecord;
|
||||
groupMemento:any;
|
||||
expressionAsString:string;
|
||||
|
||||
constructor(protoRecordRange) {
|
||||
this.protoRecordRange = protoRecordRange;
|
||||
this.headRecord = null;
|
||||
this.tailRecord = null;
|
||||
this.expressionAsString = null;
|
||||
}
|
||||
|
||||
visitImplicitReceiver(ast:ImplicitReceiver, args) {
|
||||
throw new BaseException('Should never visit an implicit receiver');
|
||||
}
|
||||
|
||||
visitLiteralPrimitive(ast:LiteralPrimitive, dest) {
|
||||
this.add(this.construct(RECORD_TYPE_CONST, ast.value, 0, null, dest));
|
||||
}
|
||||
|
||||
visitBinary(ast:Binary, dest) {
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION,
|
||||
_operationToFunction(ast.operation), 2, ast.operation, dest);
|
||||
ast.left.visit(this, new Destination(record, 0));
|
||||
ast.right.visit(this, new Destination(record, 1));
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitPrefixNot(ast:PrefixNot, dest) {
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _operation_negate, 1, "-", dest);
|
||||
ast.expression.visit(this, new Destination(record, 0));
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitAccessMember(ast:AccessMember, dest) {
|
||||
var record = this.construct(RECORD_TYPE_PROPERTY, ast.getter, 0, ast.name, dest);
|
||||
if (ast.receiver instanceof ImplicitReceiver) {
|
||||
record.setIsImplicitReceiver();
|
||||
} else {
|
||||
ast.receiver.visit(this, new Destination(record, null));
|
||||
}
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitFormatter(ast:Formatter, dest) {
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.allArgs.length, ast.name, dest);
|
||||
for (var i = 0; i < ast.allArgs.length; ++i) {
|
||||
ast.allArgs[i].visit(this, new Destination(record, i));
|
||||
}
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitMethodCall(ast:MethodCall, dest) {
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_METHOD, ast.fn, ast.args.length, ast.name, dest);
|
||||
for (var i = 0; i < ast.args.length; ++i) {
|
||||
ast.args[i].visit(this, new Destination(record, i));
|
||||
}
|
||||
if (ast.receiver instanceof ImplicitReceiver) {
|
||||
record.setIsImplicitReceiver();
|
||||
} else {
|
||||
ast.receiver.visit(this, new Destination(record, null));
|
||||
}
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitFunctionCall(ast:FunctionCall, dest) {
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_CLOSURE, null, ast.args.length, null, dest);
|
||||
ast.target.visit(this, new Destination(record, null));
|
||||
for (var i = 0; i < ast.args.length; ++i) {
|
||||
ast.args[i].visit(this, new Destination(record, i));
|
||||
}
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitCollection(ast: Collection, dest) {
|
||||
var record = this.construct(RECORD_FLAG_COLLECTION, null, null, null, dest);
|
||||
ast.value.visit(this, new Destination(record, null));
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitConditional(ast:Conditional, dest) {
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, "?:", dest);
|
||||
ast.condition.visit(this, new Destination(record, 0));
|
||||
ast.trueExp.visit(this, new Destination(record, 1));
|
||||
ast.falseExp.visit(this, new Destination(record, 2));
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitKeyedAccess(ast:KeyedAccess, dest) {
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_METHOD, _keyedAccess, 1, "[]", dest);
|
||||
ast.obj.visit(this, new Destination(record, null));
|
||||
ast.key.visit(this, new Destination(record, 0));
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitLiteralArray(ast:LiteralArray, dest) {
|
||||
var length = ast.expressions.length;
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _arrayFn(length), length, "Array()", dest);
|
||||
for (var i = 0; i < length; ++i) {
|
||||
ast.expressions[i].visit(this, new Destination(record, i));
|
||||
}
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitLiteralMap(ast:LiteralMap, dest) {
|
||||
var length = ast.values.length;
|
||||
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _mapFn(ast.keys, length), length, "Map()", dest);
|
||||
for (var i = 0; i < length; ++i) {
|
||||
ast.values[i].visit(this, new Destination(record, i));
|
||||
}
|
||||
this.add(record);
|
||||
}
|
||||
|
||||
visitChain(ast:Chain, dest){this._unsupported();}
|
||||
|
||||
visitAssignment(ast:Assignment, dest) {this._unsupported();}
|
||||
|
||||
visitTemplateBindings(ast, dest) {this._unsupported();}
|
||||
|
||||
createRecordsFromAST(ast:AST, expressionMemento:any, groupMemento:any){
|
||||
this.groupMemento = groupMemento;
|
||||
this.expressionAsString = ast.toString();
|
||||
ast.visit(this, expressionMemento);
|
||||
}
|
||||
|
||||
construct(recordType, funcOrValue, arity, name, dest) {
|
||||
return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity,
|
||||
name, dest, this.groupMemento, this.expressionAsString);
|
||||
}
|
||||
|
||||
add(protoRecord:ProtoRecord) {
|
||||
if (this.headRecord === null) {
|
||||
this.headRecord = this.tailRecord = protoRecord;
|
||||
} else {
|
||||
this.tailRecord.next = protoRecord;
|
||||
this.tailRecord = protoRecord;
|
||||
}
|
||||
}
|
||||
|
||||
_unsupported() {
|
||||
throw new BaseException("Unsupported");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _operationToFunction(operation:string):Function {
|
||||
switch(operation) {
|
||||
case '+' : return _operation_add;
|
||||
case '-' : return _operation_subtract;
|
||||
case '*' : return _operation_multiply;
|
||||
case '/' : return _operation_divide;
|
||||
case '%' : return _operation_remainder;
|
||||
case '==' : return _operation_equals;
|
||||
case '!=' : return _operation_not_equals;
|
||||
case '<' : return _operation_less_then;
|
||||
case '>' : return _operation_greater_then;
|
||||
case '<=' : return _operation_less_or_equals_then;
|
||||
case '>=' : return _operation_greater_or_equals_then;
|
||||
case '&&' : return _operation_logical_and;
|
||||
case '||' : return _operation_logical_or;
|
||||
default: throw new BaseException(`Unsupported operation ${operation}`);
|
||||
}
|
||||
}
|
||||
|
||||
function _operation_negate(value) {return !value;}
|
||||
function _operation_add(left, right) {return left + right;}
|
||||
function _operation_subtract(left, right) {return left - right;}
|
||||
function _operation_multiply(left, right) {return left * right;}
|
||||
function _operation_divide(left, right) {return left / right;}
|
||||
function _operation_remainder(left, right) {return left % right;}
|
||||
function _operation_equals(left, right) {return left == right;}
|
||||
function _operation_not_equals(left, right) {return left != right;}
|
||||
function _operation_less_then(left, right) {return left < right;}
|
||||
function _operation_greater_then(left, right) {return left > right;}
|
||||
function _operation_less_or_equals_then(left, right) {return left <= right;}
|
||||
function _operation_greater_or_equals_then(left, right) {return left >= right;}
|
||||
function _operation_logical_and(left, right) {return left && right;}
|
||||
function _operation_logical_or(left, right) {return left || right;}
|
||||
function _cond(cond, trueVal, falseVal) {return cond ? trueVal : falseVal;}
|
||||
|
||||
function _arrayFn(length:int) {
|
||||
switch (length) {
|
||||
case 0: return () => [];
|
||||
case 1: return (a1) => [a1];
|
||||
case 2: return (a1, a2) => [a1, a2];
|
||||
case 3: return (a1, a2, a3) => [a1, a2, a3];
|
||||
case 4: return (a1, a2, a3, a4) => [a1, a2, a3, a4];
|
||||
case 5: return (a1, a2, a3, a4, a5) => [a1, a2, a3, a4, a5];
|
||||
case 6: return (a1, a2, a3, a4, a5, a6) => [a1, a2, a3, a4, a5, a6];
|
||||
case 7: return (a1, a2, a3, a4, a5, a6, a7) => [a1, a2, a3, a4, a5, a6, a7];
|
||||
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => [a1, a2, a3, a4, a5, a6, a7, a8];
|
||||
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => [a1, a2, a3, a4, a5, a6, a7, a8, a9];
|
||||
default: throw new BaseException(`Does not support literal arrays with more than 9 elements`);
|
||||
}
|
||||
}
|
||||
|
||||
function _mapFn(keys:List, length:int) {
|
||||
function buildMap(values) {
|
||||
var res = StringMapWrapper.create();
|
||||
for(var i = 0; i < keys.length; ++i) {
|
||||
StringMapWrapper.set(res, keys[i], values[i]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
switch (length) {
|
||||
case 0: return () => [];
|
||||
case 1: return (a1) => buildMap([a1]);
|
||||
case 2: return (a1, a2) => buildMap([a1, a2]);
|
||||
case 3: return (a1, a2, a3) => buildMap([a1, a2, a3]);
|
||||
case 4: return (a1, a2, a3, a4) => buildMap([a1, a2, a3, a4]);
|
||||
case 5: return (a1, a2, a3, a4, a5) => buildMap([a1, a2, a3, a4, a5]);
|
||||
case 6: return (a1, a2, a3, a4, a5, a6) => buildMap([a1, a2, a3, a4, a5, a6]);
|
||||
case 7: return (a1, a2, a3, a4, a5, a6, a7) => buildMap([a1, a2, a3, a4, a5, a6, a7]);
|
||||
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8]);
|
||||
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => buildMap([a1, a2, a3, a4, a5, a6, a7, a8, a9]);
|
||||
default: throw new BaseException(`Does not support literal maps with more than 9 elements`);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: cache the getters
|
||||
function _mapGetter(key) {
|
||||
return function(map) {
|
||||
return MapWrapper.get(map, key);
|
||||
}
|
||||
}
|
||||
|
||||
function _keyedAccess(obj, args) {
|
||||
return obj[args[0]];
|
||||
}
|
|
@ -0,0 +1,477 @@
|
|||
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach} from 'test_lib/test_lib';
|
||||
|
||||
import {isPresent, isBlank, isJsObject, BaseException, FunctionWrapper} from 'facade/lang';
|
||||
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'facade/collection';
|
||||
|
||||
import {Parser} from 'change_detection/parser/parser';
|
||||
import {Lexer} from 'change_detection/parser/lexer';
|
||||
import {reflector} from 'reflection/reflection';
|
||||
import {arrayChangesAsString, kvChangesAsString} from './util';
|
||||
|
||||
import {ProtoChangeDetector, ChangeDispatcher, DynamicChangeDetector, ChangeDetectionError,
|
||||
ContextWithVariableBindings}
|
||||
from 'change_detection/change_detection';
|
||||
|
||||
|
||||
export function main() {
|
||||
function ast(exp:string, location:string = 'location') {
|
||||
var parser = new Parser(new Lexer());
|
||||
return parser.parseBinding(exp, location);
|
||||
}
|
||||
|
||||
function createChangeDetector(memo:string, exp:string, context = null, formatters = null,
|
||||
structural = false) {
|
||||
var pcd = new ProtoChangeDetector();
|
||||
pcd.addAst(ast(exp), memo, memo, structural);
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var cd = pcd.instantiate(dispatcher, formatters);
|
||||
cd.setContext(context);
|
||||
|
||||
return {"changeDetector" : cd, "dispatcher" : dispatcher};
|
||||
}
|
||||
|
||||
function executeWatch(memo:string, exp:string, context = null, formatters = null,
|
||||
content = false) {
|
||||
var res = createChangeDetector(memo, exp, context, formatters, content);
|
||||
res["changeDetector"].detectChanges();
|
||||
return res["dispatcher"].log;
|
||||
}
|
||||
|
||||
describe('change_detection', () => {
|
||||
it('should do simple watching', () => {
|
||||
var person = new Person("misko");
|
||||
var c = createChangeDetector('name', 'name', person);
|
||||
var cd = c["changeDetector"];
|
||||
var dispatcher = c["dispatcher"];
|
||||
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(['name=misko']);
|
||||
|
||||
dispatcher.clear();
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual([]);
|
||||
|
||||
person.name = "Misko";
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(['name=Misko']);
|
||||
});
|
||||
|
||||
it("should support literals", () => {
|
||||
expect(executeWatch('const', '10')).toEqual(['const=10']);
|
||||
expect(executeWatch('const', '"str"')).toEqual(['const=str']);
|
||||
});
|
||||
|
||||
it('simple chained property access', () => {
|
||||
var address = new Address('Grenoble');
|
||||
var person = new Person('Victor', address);
|
||||
|
||||
expect(executeWatch('address.city', 'address.city', person))
|
||||
.toEqual(['address.city=Grenoble']);
|
||||
});
|
||||
|
||||
it("should support method calls", () => {
|
||||
var person = new Person('Victor');
|
||||
expect(executeWatch('m', 'sayHi("Jim")', person)).toEqual(['m=Hi, Jim']);
|
||||
});
|
||||
|
||||
it("should support function calls", () => {
|
||||
var td = new TestData(() => (a) => a);
|
||||
expect(executeWatch('value', 'a()(99)', td)).toEqual(['value=99']);
|
||||
});
|
||||
|
||||
it("should support chained method calls", () => {
|
||||
var person = new Person('Victor');
|
||||
var td = new TestData(person);
|
||||
expect(executeWatch('m', 'a.sayHi("Jim")', td)).toEqual(['m=Hi, Jim']);
|
||||
});
|
||||
|
||||
it("should support literal array", () => {
|
||||
var c = createChangeDetector('array', '[1,2]');
|
||||
c["changeDetector"].detectChanges();
|
||||
expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]);
|
||||
|
||||
c = createChangeDetector('array', '[1,a]', new TestData(2));
|
||||
c["changeDetector"].detectChanges();
|
||||
expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]);
|
||||
});
|
||||
|
||||
it("should support literal maps", () => {
|
||||
var c = createChangeDetector('map', '{z:1}');
|
||||
c["changeDetector"].detectChanges();
|
||||
expect(c["dispatcher"].loggedValues[0][0]['z']).toEqual(1);
|
||||
|
||||
c = createChangeDetector('map', '{z:a}', new TestData(1));
|
||||
c["changeDetector"].detectChanges();
|
||||
expect(c["dispatcher"].loggedValues[0][0]['z']).toEqual(1);
|
||||
});
|
||||
|
||||
it("should support binary operations", () => {
|
||||
expect(executeWatch('exp', '10 + 2')).toEqual(['exp=12']);
|
||||
expect(executeWatch('exp', '10 - 2')).toEqual(['exp=8']);
|
||||
|
||||
expect(executeWatch('exp', '10 * 2')).toEqual(['exp=20']);
|
||||
expect(executeWatch('exp', '10 / 2')).toEqual([`exp=${5.0}`]); //dart exp=5.0, js exp=5
|
||||
expect(executeWatch('exp', '11 % 2')).toEqual(['exp=1']);
|
||||
|
||||
expect(executeWatch('exp', '1 == 1')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '1 != 1')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', '1 < 2')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', '2 > 1')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', '1 <= 2')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 <= 2')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 <= 1')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', '2 >= 1')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 >= 2')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '1 >= 2')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', 'true && true')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', 'true && false')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', 'true || false')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', 'false || false')).toEqual(['exp=false']);
|
||||
});
|
||||
|
||||
it("should support negate", () => {
|
||||
expect(executeWatch('exp', '!true')).toEqual(['exp=false']);
|
||||
expect(executeWatch('exp', '!!true')).toEqual(['exp=true']);
|
||||
});
|
||||
|
||||
it("should support conditionals", () => {
|
||||
expect(executeWatch('m', '1 < 2 ? 1 : 2')).toEqual(['m=1']);
|
||||
expect(executeWatch('m', '1 > 2 ? 1 : 2')).toEqual(['m=2']);
|
||||
});
|
||||
|
||||
describe("keyed access", () => {
|
||||
it("should support accessing a list item", () => {
|
||||
expect(executeWatch('array[0]', '["foo", "bar"][0]')).toEqual(['array[0]=foo']);
|
||||
});
|
||||
|
||||
it("should support accessing a map item", () => {
|
||||
expect(executeWatch('map[foo]', '{"foo": "bar"}["foo"]')).toEqual(['map[foo]=bar']);
|
||||
});
|
||||
});
|
||||
|
||||
it("should support formatters", () => {
|
||||
var formatters = MapWrapper.createFromPairs([
|
||||
['uppercase', (v) => v.toUpperCase()],
|
||||
['wrap', (v, before, after) => `${before}${v}${after}`]]);
|
||||
expect(executeWatch('str', '"aBc" | uppercase', null, formatters)).toEqual(['str=ABC']);
|
||||
expect(executeWatch('str', '"b" | wrap:"a":"c"', null, formatters)).toEqual(['str=abc']);
|
||||
});
|
||||
|
||||
describe("group changes", () => {
|
||||
it("should notify the dispatcher when a group of records changes", () => {
|
||||
var pcd = new ProtoChangeDetector();
|
||||
pcd.addAst(ast("1 + 2"), "memo", 1);
|
||||
pcd.addAst(ast("10 + 20"), "memo", 1);
|
||||
pcd.addAst(ast("100 + 200"), "memo2", 2);
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var cd = pcd.instantiate(dispatcher, null);
|
||||
|
||||
cd.detectChanges();
|
||||
|
||||
expect(dispatcher.loggedValues).toEqual([[3, 30], [300]]);
|
||||
});
|
||||
|
||||
it("should update every instance of a group individually", () => {
|
||||
var pcd = new ProtoChangeDetector();
|
||||
pcd.addAst(ast("1 + 2"), "memo", "memo");
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var cd = new DynamicChangeDetector(dispatcher, null, []);
|
||||
cd.addChild(pcd.instantiate(dispatcher, null));
|
||||
cd.addChild(pcd.instantiate(dispatcher, null));
|
||||
|
||||
cd.detectChanges();
|
||||
|
||||
expect(dispatcher.loggedValues).toEqual([[3], [3]]);
|
||||
});
|
||||
|
||||
it("should notify the dispatcher before switching to the next group", () => {
|
||||
var pcd = new ProtoChangeDetector();
|
||||
pcd.addAst(ast("a()"), "a", 1);
|
||||
pcd.addAst(ast("b()"), "b", 2);
|
||||
pcd.addAst(ast("c()"), "c", 2);
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var cd = pcd.instantiate(dispatcher, null);
|
||||
|
||||
var tr = new TestRecord();
|
||||
tr.a = () => {dispatcher.logValue('InvokeA'); return 'a'};
|
||||
tr.b = () => {dispatcher.logValue('InvokeB'); return 'b'};
|
||||
tr.c = () => {dispatcher.logValue('InvokeC'); return 'c'};
|
||||
cd.setContext(tr);
|
||||
|
||||
cd.detectChanges();
|
||||
|
||||
expect(dispatcher.loggedValues).toEqual(['InvokeA', ['a'], 'InvokeB', 'InvokeC', ['b', 'c']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enforce no new changes", () => {
|
||||
it("should throw when a record gets changed after it has been checked", () => {
|
||||
var pcd = new ProtoChangeDetector();
|
||||
pcd.addAst(ast("a"), "a", 1);
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var cd = pcd.instantiate(dispatcher, null);
|
||||
cd.setContext(new TestData('value'));
|
||||
|
||||
expect(() => {
|
||||
cd.checkNoChanges();
|
||||
}).toThrowError(new RegExp("Expression 'a in location' has changed after it was checked"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should wrap exceptions into ChangeDetectionError", () => {
|
||||
var pcd = new ProtoChangeDetector();
|
||||
pcd.addAst(ast('invalidProp', 'someComponent'), "a", 1);
|
||||
|
||||
var cd = pcd.instantiate(new TestDispatcher(), null);
|
||||
|
||||
try {
|
||||
cd.detectChanges();
|
||||
|
||||
throw new BaseException("fail");
|
||||
} catch (e) {
|
||||
expect(e).toBeAnInstanceOf(ChangeDetectionError);
|
||||
expect(e.location).toEqual("invalidProp in someComponent");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("collections", () => {
|
||||
it("should support null values", () => {
|
||||
var context = new TestData(null);
|
||||
|
||||
var c = createChangeDetector('a', 'a', context, null, true);
|
||||
var cd = c["changeDetector"];
|
||||
var dispatcher = c["dispatcher"];
|
||||
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(['a=null']);
|
||||
dispatcher.clear();
|
||||
|
||||
//cd.detectChanges();
|
||||
//expect(dispatcher.log).toEqual([]);
|
||||
|
||||
context.a = [0];
|
||||
cd.detectChanges();
|
||||
|
||||
expect(dispatcher.log).toEqual(["a=" +
|
||||
arrayChangesAsString({
|
||||
collection: ['0[null->0]'],
|
||||
additions: ['0[null->0]']
|
||||
})
|
||||
]);
|
||||
dispatcher.clear();
|
||||
|
||||
context.a = null;
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(['a=null']);
|
||||
});
|
||||
|
||||
describe("list", () => {
|
||||
it("should support list changes", () => {
|
||||
var context = new TestData([1, 2]);
|
||||
|
||||
expect(executeWatch("a", "a", context, null, true))
|
||||
.toEqual(["a=" +
|
||||
arrayChangesAsString({
|
||||
collection: ['1[null->0]', '2[null->1]'],
|
||||
additions: ['1[null->0]', '2[null->1]']
|
||||
})]);
|
||||
});
|
||||
|
||||
it("should handle reference changes", () => {
|
||||
var context = new TestData([1, 2]);
|
||||
var objs = createChangeDetector("a", "a", context, null, true);
|
||||
var cd = objs["changeDetector"];
|
||||
var dispatcher = objs["dispatcher"];
|
||||
cd.detectChanges();
|
||||
dispatcher.clear();
|
||||
|
||||
context.a = [2, 1];
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(["a=" +
|
||||
arrayChangesAsString({
|
||||
collection: ['2[1->0]', '1[0->1]'],
|
||||
previous: ['1[0->1]', '2[1->0]'],
|
||||
moves: ['2[1->0]', '1[0->1]']
|
||||
})]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("map", () => {
|
||||
it("should support map changes", () => {
|
||||
var map = MapWrapper.create();
|
||||
MapWrapper.set(map, "foo", "bar");
|
||||
var context = new TestData(map);
|
||||
expect(executeWatch("a", "a", context, null, true))
|
||||
.toEqual(["a=" +
|
||||
kvChangesAsString({
|
||||
map: ['foo[null->bar]'],
|
||||
additions: ['foo[null->bar]']
|
||||
})]);
|
||||
});
|
||||
|
||||
it("should handle reference changes", () => {
|
||||
var map = MapWrapper.create();
|
||||
MapWrapper.set(map, "foo", "bar");
|
||||
var context = new TestData(map);
|
||||
var objs = createChangeDetector("a", "a", context, null, true);
|
||||
var cd = objs["changeDetector"];
|
||||
var dispatcher = objs["dispatcher"];
|
||||
cd.detectChanges();
|
||||
dispatcher.clear();
|
||||
|
||||
context.a = MapWrapper.create();
|
||||
MapWrapper.set(context.a, "bar", "foo");
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(["a=" +
|
||||
kvChangesAsString({
|
||||
map: ['bar[null->foo]'],
|
||||
previous: ['foo[bar->null]'],
|
||||
additions: ['bar[null->foo]'],
|
||||
removals: ['foo[bar->null]']
|
||||
})]);
|
||||
});
|
||||
});
|
||||
|
||||
if (isJsObject({})) {
|
||||
describe("js objects", () => {
|
||||
it("should support object changes", () => {
|
||||
var map = {"foo": "bar"};
|
||||
var context = new TestData(map);
|
||||
expect(executeWatch("a", "a", context, null, true))
|
||||
.toEqual(["a=" +
|
||||
kvChangesAsString({
|
||||
map: ['foo[null->bar]'],
|
||||
additions: ['foo[null->bar]']
|
||||
})]);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("ContextWithVariableBindings", () => {
|
||||
it('should read a field from ContextWithVariableBindings', () => {
|
||||
var locals = new ContextWithVariableBindings(null,
|
||||
MapWrapper.createFromPairs([["key", "value"]]));
|
||||
|
||||
expect(executeWatch('key', 'key', locals))
|
||||
.toEqual(['key=value']);
|
||||
});
|
||||
|
||||
it('should handle nested ContextWithVariableBindings', () => {
|
||||
var nested = new ContextWithVariableBindings(null,
|
||||
MapWrapper.createFromPairs([["key", "value"]]));
|
||||
var locals = new ContextWithVariableBindings(nested, MapWrapper.create());
|
||||
|
||||
expect(executeWatch('key', 'key', locals))
|
||||
.toEqual(['key=value']);
|
||||
});
|
||||
|
||||
it("should fall back to a regular field read when ContextWithVariableBindings " +
|
||||
"does not have the requested field", () => {
|
||||
var locals = new ContextWithVariableBindings(new Person("Jim"),
|
||||
MapWrapper.createFromPairs([["key", "value"]]));
|
||||
|
||||
expect(executeWatch('name', 'name', locals))
|
||||
.toEqual(['name=Jim']);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestRecord {
|
||||
a;
|
||||
b;
|
||||
c;
|
||||
}
|
||||
|
||||
class Person {
|
||||
name:string;
|
||||
age:number;
|
||||
address:Address;
|
||||
constructor(name:string, address:Address = null) {
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
sayHi(m) {
|
||||
return `Hi, ${m}`;
|
||||
}
|
||||
|
||||
toString():string {
|
||||
var address = this.address == null ? '' : ' address=' + this.address.toString();
|
||||
|
||||
return 'name=' + this.name + address;
|
||||
}
|
||||
}
|
||||
|
||||
class Address {
|
||||
city:string;
|
||||
constructor(city:string) {
|
||||
this.city = city;
|
||||
}
|
||||
|
||||
toString():string {
|
||||
return this.city;
|
||||
}
|
||||
}
|
||||
|
||||
class TestData {
|
||||
a;
|
||||
|
||||
constructor(a) {
|
||||
this.a = a;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDispatcher extends ChangeDispatcher {
|
||||
log:List;
|
||||
loggedValues:List;
|
||||
onChange:Function;
|
||||
|
||||
constructor() {
|
||||
this.log = null;
|
||||
this.loggedValues = null;
|
||||
this.onChange = (_, __) => {};
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.log = ListWrapper.create();
|
||||
this.loggedValues = ListWrapper.create();
|
||||
}
|
||||
|
||||
logValue(value) {
|
||||
ListWrapper.push(this.loggedValues, value);
|
||||
}
|
||||
|
||||
onRecordChange(group, updates:List) {
|
||||
var value = updates[0].change.currentValue;
|
||||
var memento = updates[0].bindingMemento;
|
||||
ListWrapper.push(this.log, memento + '=' + this._asString(value));
|
||||
|
||||
var values = ListWrapper.map(updates, (r) => r.change.currentValue);
|
||||
ListWrapper.push(this.loggedValues, values);
|
||||
|
||||
this.onChange(group, updates);
|
||||
}
|
||||
|
||||
|
||||
_asString(value) {
|
||||
return (isBlank(value) ? 'null' : value.toString());
|
||||
}
|
||||
}
|
|
@ -1,555 +0,0 @@
|
|||
import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib';
|
||||
|
||||
import {isPresent, isBlank, isJsObject, BaseException} from 'facade/lang';
|
||||
import {List, ListWrapper, MapWrapper} from 'facade/collection';
|
||||
|
||||
import {Parser} from 'change_detection/parser/parser';
|
||||
import {Lexer} from 'change_detection/parser/lexer';
|
||||
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
|
||||
import {arrayChangesAsString, kvChangesAsString} from './util';
|
||||
|
||||
import {
|
||||
ChangeDetector,
|
||||
ProtoRecordRange,
|
||||
RecordRange,
|
||||
ChangeDispatcher,
|
||||
ProtoRecord,
|
||||
ChangeDetectionError
|
||||
} from 'change_detection/change_detector';
|
||||
|
||||
import {Record} from 'change_detection/record';
|
||||
|
||||
export function main() {
|
||||
function ast(exp:string, location:string = 'location') {
|
||||
var parser = new Parser(new Lexer());
|
||||
return parser.parseBinding(exp, location);
|
||||
}
|
||||
|
||||
function createChangeDetector(memo:string, exp:string, context = null, formatters = null,
|
||||
content = false) {
|
||||
var prr = new ProtoRecordRange();
|
||||
prr.addRecordsFromAST(ast(exp), memo, memo, content);
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var rr = prr.instantiate(dispatcher, formatters);
|
||||
rr.setContext(context);
|
||||
|
||||
var cd = new ChangeDetector(rr);
|
||||
|
||||
return {"changeDetector" : cd, "dispatcher" : dispatcher};
|
||||
}
|
||||
|
||||
function executeWatch(memo:string, exp:string, context = null, formatters = null,
|
||||
content = false) {
|
||||
var res = createChangeDetector(memo, exp, context, formatters, content);
|
||||
res["changeDetector"].detectChanges();
|
||||
return res["dispatcher"].log;
|
||||
}
|
||||
|
||||
describe('change_detection', () => {
|
||||
describe('ChangeDetection', () => {
|
||||
function createRange(dispatcher, ast, group) {
|
||||
var prr = new ProtoRecordRange();
|
||||
prr.addRecordsFromAST(ast, "memo", group);
|
||||
return prr.instantiate(dispatcher, null);
|
||||
}
|
||||
|
||||
function detectChangesInRange(recordRange) {
|
||||
var cd = new ChangeDetector(recordRange);
|
||||
cd.detectChanges();
|
||||
}
|
||||
|
||||
it('should do simple watching', () => {
|
||||
var person = new Person("misko");
|
||||
|
||||
var c = createChangeDetector('name', 'name', person);
|
||||
var cd = c["changeDetector"];
|
||||
var dispatcher = c["dispatcher"];
|
||||
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(['name=misko']);
|
||||
|
||||
dispatcher.clear();
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual([]);
|
||||
|
||||
person.name = "Misko";
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(['name=Misko']);
|
||||
});
|
||||
|
||||
it('should support chained properties', () => {
|
||||
var address = new Address('Grenoble');
|
||||
var person = new Person('Victor', address);
|
||||
|
||||
expect(executeWatch('address.city', 'address.city', person))
|
||||
.toEqual(['address.city=Grenoble']);
|
||||
});
|
||||
|
||||
it("should support method calls", () => {
|
||||
var person = new Person('Victor');
|
||||
expect(executeWatch('m', 'sayHi("Jim")', person)).toEqual(['m=Hi, Jim']);
|
||||
});
|
||||
|
||||
it("should support function calls", () => {
|
||||
var td = new TestData(() => (a) => a);
|
||||
expect(executeWatch('value', 'a()(99)', td)).toEqual(['value=99']);
|
||||
});
|
||||
|
||||
it("should support chained method calls", () => {
|
||||
var person = new Person('Victor');
|
||||
var td = new TestData(person);
|
||||
expect(executeWatch('m', 'a.sayHi("Jim")', td)).toEqual(['m=Hi, Jim']);
|
||||
});
|
||||
|
||||
it("should support literals", () => {
|
||||
expect(executeWatch('const', '10')).toEqual(['const=10']);
|
||||
expect(executeWatch('const', '"str"')).toEqual(['const=str']);
|
||||
});
|
||||
|
||||
it("should support literal array", () => {
|
||||
var c = createChangeDetector('array', '[1,2]');
|
||||
c["changeDetector"].detectChanges();
|
||||
expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]);
|
||||
|
||||
c = createChangeDetector('array', '[1,a]', new TestData(2));
|
||||
c["changeDetector"].detectChanges();
|
||||
expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]);
|
||||
});
|
||||
|
||||
it("should support literal maps", () => {
|
||||
var c = createChangeDetector('map', '{z:1}');
|
||||
c["changeDetector"].detectChanges();
|
||||
expect(c["dispatcher"].loggedValues[0][0]['z']).toEqual(1);
|
||||
|
||||
c = createChangeDetector('map', '{z:a}', new TestData(1));
|
||||
c["changeDetector"].detectChanges();
|
||||
expect(c["dispatcher"].loggedValues[0][0]['z']).toEqual(1);
|
||||
});
|
||||
|
||||
it("should support binary operations", () => {
|
||||
expect(executeWatch('exp', '10 + 2')).toEqual(['exp=12']);
|
||||
expect(executeWatch('exp', '10 - 2')).toEqual(['exp=8']);
|
||||
|
||||
expect(executeWatch('exp', '10 * 2')).toEqual(['exp=20']);
|
||||
expect(executeWatch('exp', '10 / 2')).toEqual([`exp=${5.0}`]); //dart exp=5.0, js exp=5
|
||||
expect(executeWatch('exp', '11 % 2')).toEqual(['exp=1']);
|
||||
|
||||
expect(executeWatch('exp', '1 == 1')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '1 != 1')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', '1 < 2')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', '2 > 1')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 < 1')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', '1 <= 2')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 <= 2')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 <= 1')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', '2 >= 1')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '2 >= 2')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', '1 >= 2')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', 'true && true')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', 'true && false')).toEqual(['exp=false']);
|
||||
|
||||
expect(executeWatch('exp', 'true || false')).toEqual(['exp=true']);
|
||||
expect(executeWatch('exp', 'false || false')).toEqual(['exp=false']);
|
||||
});
|
||||
|
||||
it("should support negate", () => {
|
||||
expect(executeWatch('exp', '!true')).toEqual(['exp=false']);
|
||||
expect(executeWatch('exp', '!!true')).toEqual(['exp=true']);
|
||||
});
|
||||
|
||||
it("should support conditionals", () => {
|
||||
expect(executeWatch('m', '1 < 2 ? 1 : 2')).toEqual(['m=1']);
|
||||
expect(executeWatch('m', '1 > 2 ? 1 : 2')).toEqual(['m=2']);
|
||||
});
|
||||
|
||||
describe("keyed access", () => {
|
||||
it("should support accessing a list item", () => {
|
||||
expect(executeWatch('array[0]', '["foo", "bar"][0]')).toEqual(['array[0]=foo']);
|
||||
});
|
||||
it("should support accessing a map item", () => {
|
||||
expect(executeWatch('map[foo]', '{"foo": "bar"}["foo"]')).toEqual(['map[foo]=bar']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatters", () => {
|
||||
it("should support formatters", () => {
|
||||
var formatters = MapWrapper.createFromPairs([
|
||||
['uppercase', (v) => v.toUpperCase()],
|
||||
['wrap', (v, before, after) => `${before}${v}${after}`]]);
|
||||
expect(executeWatch('str', '"aBc" | uppercase', null, formatters)).toEqual(['str=ABC']);
|
||||
expect(executeWatch('str', '"b" | wrap:"a":"c"', null, formatters)).toEqual(['str=abc']);
|
||||
});
|
||||
|
||||
it("should rerun formatters only when arguments change", () => {
|
||||
var counter = 0;
|
||||
var formatters = MapWrapper.createFromPairs([
|
||||
['formatter', (_) => {counter += 1; return 'value'}]
|
||||
]);
|
||||
|
||||
var person = new Person('Jim');
|
||||
|
||||
var c = createChangeDetector('formatter', 'name | formatter', person, formatters);
|
||||
var cd = c['changeDetector'];
|
||||
|
||||
cd.detectChanges();
|
||||
expect(counter).toEqual(1);
|
||||
|
||||
cd.detectChanges();
|
||||
expect(counter).toEqual(1);
|
||||
|
||||
person.name = 'bob';
|
||||
cd.detectChanges();
|
||||
expect(counter).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("ContextWithVariableBindings", () => {
|
||||
it('should read a field from ContextWithVariableBindings', () => {
|
||||
var locals = new ContextWithVariableBindings(null,
|
||||
MapWrapper.createFromPairs([["key", "value"]]));
|
||||
|
||||
expect(executeWatch('key', 'key', locals))
|
||||
.toEqual(['key=value']);
|
||||
});
|
||||
|
||||
it('should handle nested ContextWithVariableBindings', () => {
|
||||
var nested = new ContextWithVariableBindings(null,
|
||||
MapWrapper.createFromPairs([["key", "value"]]));
|
||||
var locals = new ContextWithVariableBindings(nested, MapWrapper.create());
|
||||
|
||||
expect(executeWatch('key', 'key', locals))
|
||||
.toEqual(['key=value']);
|
||||
});
|
||||
|
||||
it("should fall back to a regular field read when ContextWithVariableBindings " +
|
||||
"does not have the requested field", () => {
|
||||
var locals = new ContextWithVariableBindings(new Person("Jim"),
|
||||
MapWrapper.createFromPairs([["key", "value"]]));
|
||||
|
||||
expect(executeWatch('name', 'name', locals))
|
||||
.toEqual(['name=Jim']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collections", () => {
|
||||
it("should support null values", () => {
|
||||
var context = new TestData(null);
|
||||
var c = createChangeDetector('a', 'a', context, null, true);
|
||||
var cd = c["changeDetector"];
|
||||
var dsp = c["dispatcher"];
|
||||
|
||||
cd.detectChanges();
|
||||
expect(dsp.log).toEqual(['a=null']);
|
||||
dsp.clear();
|
||||
|
||||
cd.detectChanges();
|
||||
expect(dsp.log).toEqual([]);
|
||||
|
||||
context.a = [0];
|
||||
cd.detectChanges();
|
||||
|
||||
expect(dsp.log).toEqual(["a=" +
|
||||
arrayChangesAsString({
|
||||
collection: ['0[null->0]'],
|
||||
additions: ['0[null->0]']
|
||||
})
|
||||
]);
|
||||
dsp.clear();
|
||||
|
||||
context.a = null;
|
||||
cd.detectChanges();
|
||||
expect(dsp.log).toEqual(['a=null']);
|
||||
});
|
||||
|
||||
it("should throw if not collection / null", () => {
|
||||
var context = new TestData("not collection / null");
|
||||
var c = createChangeDetector('a', 'a', context, null, true);
|
||||
expect(() => c["changeDetector"].detectChanges())
|
||||
.toThrowError(new RegExp("Collection records must be array like, map like or null"));
|
||||
});
|
||||
|
||||
describe("list", () => {
|
||||
it("should support list changes", () => {
|
||||
var context = new TestData([1, 2]);
|
||||
expect(executeWatch("a", "a", context, null, true))
|
||||
.toEqual(["a=" +
|
||||
arrayChangesAsString({
|
||||
collection: ['1[null->0]', '2[null->1]'],
|
||||
additions: ['1[null->0]', '2[null->1]']
|
||||
})]);
|
||||
});
|
||||
|
||||
it("should handle reference changes", () => {
|
||||
var context = new TestData([1, 2]);
|
||||
var objs = createChangeDetector("a", "a", context, null, true);
|
||||
var cd = objs["changeDetector"];
|
||||
var dispatcher = objs["dispatcher"];
|
||||
cd.detectChanges();
|
||||
dispatcher.clear();
|
||||
|
||||
context.a = [2, 1];
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(["a=" +
|
||||
arrayChangesAsString({
|
||||
collection: ['2[1->0]', '1[0->1]'],
|
||||
previous: ['1[0->1]', '2[1->0]'],
|
||||
moves: ['2[1->0]', '1[0->1]']
|
||||
})]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("map", () => {
|
||||
it("should support map changes", () => {
|
||||
var map = MapWrapper.create();
|
||||
MapWrapper.set(map, "foo", "bar");
|
||||
var context = new TestData(map);
|
||||
expect(executeWatch("a", "a", context, null, true))
|
||||
.toEqual(["a=" +
|
||||
kvChangesAsString({
|
||||
map: ['foo[null->bar]'],
|
||||
additions: ['foo[null->bar]']
|
||||
})]);
|
||||
});
|
||||
|
||||
it("should handle reference changes", () => {
|
||||
var map = MapWrapper.create();
|
||||
MapWrapper.set(map, "foo", "bar");
|
||||
var context = new TestData(map);
|
||||
var objs = createChangeDetector("a", "a", context, null, true);
|
||||
var cd = objs["changeDetector"];
|
||||
var dispatcher = objs["dispatcher"];
|
||||
cd.detectChanges();
|
||||
dispatcher.clear();
|
||||
|
||||
context.a = MapWrapper.create();
|
||||
MapWrapper.set(context.a, "bar", "foo");
|
||||
cd.detectChanges();
|
||||
expect(dispatcher.log).toEqual(["a=" +
|
||||
kvChangesAsString({
|
||||
map: ['bar[null->foo]'],
|
||||
previous: ['foo[bar->null]'],
|
||||
additions: ['bar[null->foo]'],
|
||||
removals: ['foo[bar->null]']
|
||||
})]);
|
||||
});
|
||||
});
|
||||
|
||||
if (isJsObject({})) {
|
||||
describe("js objects", () => {
|
||||
it("should support object changes", () => {
|
||||
var map = {"foo": "bar"};
|
||||
var context = new TestData(map);
|
||||
expect(executeWatch("a", "a", context, null, true))
|
||||
.toEqual(["a=" +
|
||||
kvChangesAsString({
|
||||
map: ['foo[null->bar]'],
|
||||
additions: ['foo[null->bar]']
|
||||
})]);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("adding new ranges", () => {
|
||||
var dispatcher;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatcher = new TestDispatcher();
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that we can add a new range after the current
|
||||
* record has been disabled. The new range must be processed
|
||||
* during the same change detection run.
|
||||
*/
|
||||
it("should work when disabling the last enabled record", () => {
|
||||
var rr = createRange(dispatcher, ast("1"), 1);
|
||||
|
||||
dispatcher.onChange = (group, _) => {
|
||||
if (group === 1) { // to prevent infinite loop
|
||||
var rangeToAppend = createRange(dispatcher, ast("2"), 2);
|
||||
rr.addRange(rangeToAppend);
|
||||
}
|
||||
};
|
||||
|
||||
detectChangesInRange(rr);
|
||||
|
||||
expect(dispatcher.loggedValues).toEqual([[1], [2]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("group changes", () => {
|
||||
it("should notify the dispatcher when a group of records changes", () => {
|
||||
var prr = new ProtoRecordRange();
|
||||
prr.addRecordsFromAST(ast("1 + 2"), "memo", 1);
|
||||
prr.addRecordsFromAST(ast("10 + 20"), "memo", 1);
|
||||
prr.addRecordsFromAST(ast("100 + 200"), "memo2", 2);
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var rr = prr.instantiate(dispatcher, null);
|
||||
|
||||
detectChangesInRange(rr);
|
||||
|
||||
expect(dispatcher.loggedValues).toEqual([[3, 30], [300]]);
|
||||
});
|
||||
|
||||
it("should update every instance of a group individually", () => {
|
||||
var prr = new ProtoRecordRange();
|
||||
prr.addRecordsFromAST(ast("1 + 2"), "memo", "memo");
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var rr = new RecordRange(null, dispatcher);
|
||||
rr.addRange(prr.instantiate(dispatcher, null));
|
||||
rr.addRange(prr.instantiate(dispatcher, null));
|
||||
|
||||
detectChangesInRange(rr);
|
||||
|
||||
expect(dispatcher.loggedValues).toEqual([[3], [3]]);
|
||||
});
|
||||
|
||||
it("should notify the dispatcher before switching to the next group", () => {
|
||||
var prr = new ProtoRecordRange();
|
||||
prr.addRecordsFromAST(ast("a()"), "a", 1);
|
||||
prr.addRecordsFromAST(ast("b()"), "b", 2);
|
||||
prr.addRecordsFromAST(ast("c()"), "c", 2);
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var rr = prr.instantiate(dispatcher, null);
|
||||
|
||||
var tr = new TestRecord();
|
||||
tr.a = () => {dispatcher.logValue('InvokeA'); return 'a'};
|
||||
tr.b = () => {dispatcher.logValue('InvokeB'); return 'b'};
|
||||
tr.c = () => {dispatcher.logValue('InvokeC'); return 'c'};
|
||||
rr.setContext(tr);
|
||||
|
||||
detectChangesInRange(rr);
|
||||
|
||||
expect(dispatcher.loggedValues).toEqual(['InvokeA', ['a'], 'InvokeB', 'InvokeC', ['b', 'c']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enforce no new changes", () => {
|
||||
it("should throw when a record gets changed after it has been checked", () => {
|
||||
var prr = new ProtoRecordRange();
|
||||
prr.addRecordsFromAST(ast("a"), "a", 1);
|
||||
prr.addRecordsFromAST(ast("b()"), "b", 2);
|
||||
|
||||
var tr = new TestRecord();
|
||||
tr.a = "a";
|
||||
tr.b = () => {tr.a = "newA";};
|
||||
|
||||
var dispatcher = new TestDispatcher();
|
||||
var rr = prr.instantiate(dispatcher, null);
|
||||
rr.setContext(tr);
|
||||
|
||||
expect(() => {
|
||||
var cd = new ChangeDetector(rr, true);
|
||||
cd.detectChanges();
|
||||
}).toThrowError(new RegExp("Expression 'a in location' has changed after it was checked"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should wrap exceptions into ChangeDetectionError", () => {
|
||||
try {
|
||||
var rr = createRange(new TestDispatcher(), ast("invalidProp", "someComponent"), 1);
|
||||
detectChangesInRange(rr);
|
||||
|
||||
throw new BaseException("fail");
|
||||
} catch (e) {
|
||||
expect(e).toBeAnInstanceOf(ChangeDetectionError);
|
||||
expect(e.location).toEqual("invalidProp in someComponent");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestRecord {
|
||||
a;
|
||||
b;
|
||||
c;
|
||||
}
|
||||
|
||||
class Person {
|
||||
name:string;
|
||||
address:Address;
|
||||
constructor(name:string, address:Address = null) {
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
sayHi(m) {
|
||||
return `Hi, ${m}`;
|
||||
}
|
||||
|
||||
toString():string {
|
||||
var address = this.address == null ? '' : ' address=' + this.address.toString();
|
||||
|
||||
return 'name=' + this.name + address;
|
||||
}
|
||||
}
|
||||
|
||||
class Address {
|
||||
city:string;
|
||||
constructor(city:string) {
|
||||
this.city = city;
|
||||
}
|
||||
|
||||
toString():string {
|
||||
return this.city;
|
||||
}
|
||||
}
|
||||
|
||||
class TestData {
|
||||
a;
|
||||
constructor(a) {
|
||||
this.a = a;
|
||||
}
|
||||
}
|
||||
|
||||
class TestDispatcher extends ChangeDispatcher {
|
||||
log:List;
|
||||
loggedValues:List;
|
||||
onChange:Function;
|
||||
|
||||
constructor() {
|
||||
this.log = null;
|
||||
this.loggedValues = null;
|
||||
this.onChange = (_, __) => {};
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.log = ListWrapper.create();
|
||||
this.loggedValues = ListWrapper.create();
|
||||
}
|
||||
|
||||
logValue(value) {
|
||||
ListWrapper.push(this.loggedValues, value);
|
||||
}
|
||||
|
||||
onRecordChange(group, records:List) {
|
||||
var value = records[0].currentValue;
|
||||
var dest = records[0].protoRecord.dest;
|
||||
ListWrapper.push(this.log, dest + '=' + this._asString(value));
|
||||
|
||||
var values = ListWrapper.map(records, (r) => r.currentValue);
|
||||
ListWrapper.push(this.loggedValues, values);
|
||||
|
||||
this.onChange(group, records);
|
||||
}
|
||||
|
||||
_asString(value) {
|
||||
return (isBlank(value) ? 'null' : value.toString());
|
||||
}
|
||||
}
|
|
@ -1,364 +0,0 @@
|
|||
import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib';
|
||||
|
||||
import {Parser} from 'change_detection/parser/parser';
|
||||
import {Lexer} from 'change_detection/parser/lexer';
|
||||
|
||||
import {List, ListWrapper, MapWrapper} from 'facade/collection';
|
||||
import {isPresent} from 'facade/lang';
|
||||
|
||||
import {
|
||||
ChangeDetector,
|
||||
ProtoRecordRange,
|
||||
RecordRange,
|
||||
ProtoRecord,
|
||||
RECORD_TYPE_CONST
|
||||
} from 'change_detection/change_detector';
|
||||
|
||||
import {Record} from 'change_detection/record';
|
||||
|
||||
export function main() {
|
||||
var lookupName = (names, item) =>
|
||||
ListWrapper.last(
|
||||
ListWrapper.find(names, (pair) => pair[0] === item));
|
||||
|
||||
function enabledRecordsInReverseOrder(rr:RecordRange, names:List) {
|
||||
var reversed = [];
|
||||
var record = rr.findLastEnabledRecord();
|
||||
while (isPresent(record)) {
|
||||
ListWrapper.push(reversed, lookupName(names, record));
|
||||
record = record.prevEnabled;
|
||||
}
|
||||
return reversed;
|
||||
}
|
||||
|
||||
function enabledRecords(rr:RecordRange, names:List) {
|
||||
var res = [];
|
||||
var record = rr.findFirstEnabledRecord();
|
||||
while (isPresent(record)) {
|
||||
ListWrapper.push(res, lookupName(names, record));
|
||||
record = record.nextEnabled;
|
||||
}
|
||||
|
||||
// check that all links are set properly in both directions
|
||||
var reversed = enabledRecordsInReverseOrder(rr, names);
|
||||
expect(res).toEqual(ListWrapper.reversed(reversed));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function createRecord(rr) {
|
||||
return new Record(rr, new ProtoRecord(null, 0, null, null, null, null, null, null), null);
|
||||
}
|
||||
|
||||
describe('record range', () => {
|
||||
it('should add records', () => {
|
||||
var rr = new RecordRange(null, null);
|
||||
var record1 = createRecord(rr);
|
||||
var record2 = createRecord(rr);
|
||||
|
||||
rr.addRecord(record1);
|
||||
rr.addRecord(record2);
|
||||
|
||||
expect(enabledRecords(rr, [
|
||||
[record1, 'record1'],
|
||||
[record2, 'record2']
|
||||
])).toEqual(['record1', 'record2']);
|
||||
});
|
||||
|
||||
describe('adding/removing record ranges', () => {
|
||||
var parent, child1, child2, child3;
|
||||
var childRecord1, childRecord2, childRecord3;
|
||||
var recordNames;
|
||||
|
||||
beforeEach(() => {
|
||||
parent = new RecordRange(null, null);
|
||||
|
||||
child1 = new RecordRange(null, null);
|
||||
childRecord1 = createRecord(child1);
|
||||
child1.addRecord(childRecord1);
|
||||
|
||||
child2 = new RecordRange(null, null);
|
||||
childRecord2 = createRecord(child2);
|
||||
child2.addRecord(childRecord2);
|
||||
|
||||
child3 = new RecordRange(null, null);
|
||||
childRecord3 = createRecord(child3);
|
||||
child3.addRecord(childRecord3);
|
||||
|
||||
recordNames = [
|
||||
[childRecord1, 'record1'],
|
||||
[childRecord2, 'record2'],
|
||||
[childRecord3, 'record3']
|
||||
];
|
||||
});
|
||||
|
||||
it('should add record ranges', () => {
|
||||
parent.addRange(child1);
|
||||
parent.addRange(child2);
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']);
|
||||
});
|
||||
|
||||
it('should handle adding an empty range', () => {
|
||||
var emptyRange = new RecordRange(null, null);
|
||||
parent.addRange(child1);
|
||||
parent.addRange(child2);
|
||||
child1.addRange(emptyRange);
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']);
|
||||
});
|
||||
|
||||
it('should handle enabling/disabling an empty range', () => {
|
||||
var emptyRange = new RecordRange(null, null);
|
||||
emptyRange.disable();
|
||||
emptyRange.enable();
|
||||
|
||||
expect(enabledRecords(emptyRange, recordNames)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle adding a range into an empty range', () => {
|
||||
var emptyRange = new RecordRange(null, null);
|
||||
parent.addRange(emptyRange);
|
||||
parent.addRange(child2);
|
||||
|
||||
emptyRange.addRange(child1);
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']);
|
||||
});
|
||||
|
||||
it('should add nested record ranges', () => {
|
||||
parent.addRange(child1);
|
||||
child1.addRange(child2);
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']);
|
||||
});
|
||||
|
||||
it('should remove record ranges', () => {
|
||||
parent.addRange(child1);
|
||||
parent.addRange(child2);
|
||||
|
||||
child1.remove();
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record2']);
|
||||
|
||||
child2.remove();
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should remove an empty record range', () => {
|
||||
var emptyRange = new RecordRange(null, null);
|
||||
parent.addRange(child1);
|
||||
parent.addRange(emptyRange);
|
||||
parent.addRange(child2);
|
||||
|
||||
emptyRange.remove();
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record2']);
|
||||
});
|
||||
|
||||
it('should remove a record range surrounded by other ranges', () => {
|
||||
parent.addRange(child1);
|
||||
parent.addRange(child2);
|
||||
parent.addRange(child3);
|
||||
|
||||
child2.remove();
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabling/disabling records', () => {
|
||||
var rr;
|
||||
var record1, record2, record3, record4;
|
||||
var recordNames;
|
||||
|
||||
beforeEach(() => {
|
||||
rr = new RecordRange(null, null);
|
||||
record1 = createRecord(rr);
|
||||
record2 = createRecord(rr);
|
||||
record3 = createRecord(rr);
|
||||
record4 = createRecord(rr);
|
||||
|
||||
recordNames = [
|
||||
[record1, 'record1'],
|
||||
[record2, 'record2'],
|
||||
[record3, 'record3'],
|
||||
[record4, 'record4']
|
||||
];
|
||||
});
|
||||
|
||||
it('should disable a single record', () => {
|
||||
rr.addRecord(record1);
|
||||
|
||||
record1.disable();
|
||||
|
||||
expect(enabledRecords(rr, recordNames)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should enable a single record', () => {
|
||||
rr.addRecord(record1);
|
||||
record1.disable();
|
||||
|
||||
record1.enable();
|
||||
|
||||
expect(enabledRecords(rr, recordNames)).toEqual(['record1']);
|
||||
});
|
||||
|
||||
it('should disable a record', () => {
|
||||
rr.addRecord(record1);
|
||||
rr.addRecord(record2);
|
||||
rr.addRecord(record3);
|
||||
rr.addRecord(record4);
|
||||
|
||||
record2.disable();
|
||||
record3.disable();
|
||||
|
||||
expect(record2.isDisabled()).toBeTruthy();
|
||||
expect(record3.isDisabled()).toBeTruthy();
|
||||
|
||||
expect(enabledRecords(rr, recordNames)).toEqual(['record1', 'record4']);
|
||||
});
|
||||
|
||||
it('should enable a record', () => {
|
||||
rr.addRecord(record1);
|
||||
rr.addRecord(record2);
|
||||
rr.addRecord(record3);
|
||||
rr.addRecord(record4);
|
||||
record2.disable();
|
||||
record3.disable();
|
||||
|
||||
record2.enable();
|
||||
record3.enable();
|
||||
|
||||
expect(enabledRecords(rr, recordNames)).toEqual(['record1', 'record2', 'record3', 'record4']);
|
||||
});
|
||||
|
||||
it('should disable a single record in a range', () => {
|
||||
var rr1 = new RecordRange(null, null);
|
||||
rr1.addRecord(record1);
|
||||
|
||||
var rr2 = new RecordRange(null, null);
|
||||
rr2.addRecord(record2);
|
||||
|
||||
var rr3 = new RecordRange(null, null);
|
||||
rr3.addRecord(record3);
|
||||
|
||||
rr.addRange(rr1);
|
||||
rr.addRange(rr2);
|
||||
rr.addRange(rr3);
|
||||
|
||||
record2.disable();
|
||||
|
||||
expect(enabledRecords(rr, recordNames)).toEqual(['record1', 'record3']);
|
||||
|
||||
record2.enable();
|
||||
|
||||
expect(enabledRecords(rr, recordNames)).toEqual(['record1', 'record2', 'record3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabling/disabling record ranges', () => {
|
||||
var child1, child2, child3, child4;
|
||||
var record1, record2, record3, record4;
|
||||
var recordNames;
|
||||
|
||||
beforeEach(() => {
|
||||
child1 = new RecordRange(null, null);
|
||||
record1 = createRecord(child1);
|
||||
child1.addRecord(record1);
|
||||
|
||||
child2 = new RecordRange(null, null);
|
||||
record2 = createRecord(child2);
|
||||
child2.addRecord(record2);
|
||||
|
||||
child3 = new RecordRange(null, null);
|
||||
record3 = createRecord(child3);
|
||||
child3.addRecord(record3);
|
||||
|
||||
child4 = new RecordRange(null, null);
|
||||
record4 = createRecord(child4);
|
||||
child4.addRecord(record4);
|
||||
|
||||
recordNames = [
|
||||
[record1, 'record1'],
|
||||
[record2, 'record2'],
|
||||
[record3, 'record3'],
|
||||
[record4, 'record4']
|
||||
];
|
||||
});
|
||||
|
||||
it('should disable a single record range', () => {
|
||||
var parent = new RecordRange(null, null);
|
||||
parent.addRange(child1);
|
||||
|
||||
child1.disable();
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should enable a single record range', () => {
|
||||
var parent = new RecordRange(null, null);
|
||||
parent.addRange(child1);
|
||||
|
||||
child1.disable();
|
||||
|
||||
child1.enable();
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record1']);
|
||||
});
|
||||
|
||||
it('should disable a record range', () => {
|
||||
var parent = new RecordRange(null, null);
|
||||
parent.addRange(child1);
|
||||
parent.addRange(child2);
|
||||
parent.addRange(child3);
|
||||
parent.addRange(child4);
|
||||
|
||||
child2.disable();
|
||||
child3.disable();
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual(['record1', 'record4']);
|
||||
});
|
||||
|
||||
it('should enable a record range', () => {
|
||||
var parent = new RecordRange(null, null);
|
||||
parent.addRange(child1);
|
||||
parent.addRange(child2);
|
||||
parent.addRange(child3);
|
||||
parent.addRange(child4);
|
||||
|
||||
child2.disable();
|
||||
child2.disable();
|
||||
|
||||
child2.enable();
|
||||
child3.enable();
|
||||
|
||||
expect(enabledRecords(parent, recordNames)).toEqual([
|
||||
'record1', 'record2', 'record3', 'record4'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("inspect", () => {
|
||||
it("should return the description of the record", () => {
|
||||
var proto = new ProtoRecord(null, RECORD_TYPE_CONST, 1, 0, "name", null, "group", "expression");
|
||||
var record = new Record(null, proto, null);
|
||||
|
||||
var i = record.inspect();
|
||||
expect(i.description).toContain("const, name, enabled");
|
||||
});
|
||||
|
||||
it("should return the description of the records in the range", () => {
|
||||
var proto = new ProtoRecord(null, RECORD_TYPE_CONST, 1, 0, "name", null, "group", "expression");
|
||||
var record = new Record(null, proto, null);
|
||||
var range = new RecordRange(null, null);
|
||||
range.addRecord(record);
|
||||
|
||||
var i = range.inspect();;
|
||||
expect(i.length).toEqual(1);
|
||||
expect(i[0]).toContain("const, name, enabled");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -4,7 +4,7 @@ import {DOM, Element} from 'facade/dom';
|
|||
import {Compiler, CompilerCache} from './compiler/compiler';
|
||||
import {ProtoView} from './compiler/view';
|
||||
import {Reflector, reflector} from 'reflection/reflection';
|
||||
import {Parser, Lexer, ChangeDetector, RecordRange} from 'change_detection/change_detection';
|
||||
import {Parser, Lexer, ChangeDetector} from 'change_detection/change_detection';
|
||||
import {TemplateLoader} from './compiler/template_loader';
|
||||
import {DirectiveMetadataReader} from './compiler/directive_metadata_reader';
|
||||
import {DirectiveMetadata} from './compiler/directive_metadata';
|
||||
|
@ -21,7 +21,7 @@ var _rootBindings = [
|
|||
];
|
||||
|
||||
export var appViewToken = new OpaqueToken('AppView');
|
||||
export var appRecordRangeToken = new OpaqueToken('AppRecordRange');
|
||||
export var appChangeDetectorToken = new OpaqueToken('AppChangeDetector');
|
||||
export var appElementToken = new OpaqueToken('AppElement');
|
||||
export var appComponentAnnotatedTypeToken = new OpaqueToken('AppComponentAnnotatedType');
|
||||
export var appDocumentToken = new OpaqueToken('AppDocument');
|
||||
|
@ -60,13 +60,11 @@ function _injectorBindings(appComponentType) {
|
|||
});
|
||||
}, [Compiler, Injector, appElementToken, appComponentAnnotatedTypeToken]),
|
||||
|
||||
bind(appRecordRangeToken).toFactory((rootView) => rootView.recordRange,
|
||||
bind(appChangeDetectorToken).toFactory((rootView) => rootView.changeDetector,
|
||||
[appViewToken]),
|
||||
bind(ChangeDetector).toFactory((appRecordRange) =>
|
||||
new ChangeDetector(appRecordRange, assertionsEnabled()), [appRecordRangeToken]),
|
||||
bind(appComponentType).toFactory((rootView) => rootView.elementInjectors[0].getComponent(),
|
||||
[appViewToken]),
|
||||
bind(LifeCycle).toClass(LifeCycle)
|
||||
bind(LifeCycle).toFactory((cd) => new LifeCycle(cd, assertionsEnabled()), [appChangeDetectorToken])
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {ElementBinder} from '../element_binder';
|
|||
import {ProtoElementInjector} from '../element_injector';
|
||||
import {ProtoView} from '../view';
|
||||
|
||||
import {ASTWithSource} from 'change_detection/change_detection';
|
||||
import {AST} from 'change_detection/change_detection';
|
||||
|
||||
/**
|
||||
* Collects all data that is needed to process an element
|
||||
|
@ -86,14 +86,14 @@ export class CompileElement {
|
|||
return this._classList;
|
||||
}
|
||||
|
||||
addTextNodeBinding(indexInParent:int, expression:ASTWithSource) {
|
||||
addTextNodeBinding(indexInParent:int, expression:AST) {
|
||||
if (isBlank(this.textNodeBindings)) {
|
||||
this.textNodeBindings = MapWrapper.create();
|
||||
}
|
||||
MapWrapper.set(this.textNodeBindings, indexInParent, expression);
|
||||
}
|
||||
|
||||
addPropertyBinding(property:string, expression:ASTWithSource) {
|
||||
addPropertyBinding(property:string, expression:AST) {
|
||||
if (isBlank(this.propertyBindings)) {
|
||||
this.propertyBindings = MapWrapper.create();
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ export class CompileElement {
|
|||
MapWrapper.set(this.variableBindings, contextName, templateName);
|
||||
}
|
||||
|
||||
addEventBinding(eventName:string, expression:ASTWithSource) {
|
||||
addEventBinding(eventName:string, expression:AST) {
|
||||
if (isBlank(this.eventBindings)) {
|
||||
this.eventBindings = MapWrapper.create();
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import {ListWrapper, List, MapWrapper, StringMapWrapper} from 'facade/collection
|
|||
|
||||
import {reflector} from 'reflection/reflection';
|
||||
|
||||
import {Parser, ProtoRecordRange} from 'change_detection/change_detection';
|
||||
import {Parser, ProtoChangeDetector} from 'change_detection/change_detection';
|
||||
|
||||
import {Component, Directive} from '../../annotations/annotations';
|
||||
import {DirectiveMetadata} from '../directive_metadata';
|
||||
|
@ -18,7 +18,7 @@ import {CompileControl} from './compile_control';
|
|||
|
||||
/**
|
||||
* Creates the ElementBinders and adds watches to the
|
||||
* ProtoRecordRange.
|
||||
* ProtoChangeDetector.
|
||||
*
|
||||
* Fills:
|
||||
* - CompileElement#inheritedElementBinder
|
||||
|
|
|
@ -2,7 +2,7 @@ import {isPresent, BaseException} from 'facade/lang';
|
|||
import {ListWrapper, MapWrapper} from 'facade/collection';
|
||||
|
||||
import {ProtoView} from '../view';
|
||||
import {ProtoRecordRange} from 'change_detection/change_detection';
|
||||
import {ProtoChangeDetector} from 'change_detection/change_detection';
|
||||
|
||||
import {CompileStep} from './compile_step';
|
||||
import {CompileElement} from './compile_element';
|
||||
|
@ -21,7 +21,7 @@ export class ProtoViewBuilder extends CompileStep {
|
|||
process(parent:CompileElement, current:CompileElement, control:CompileControl) {
|
||||
var inheritedProtoView = null;
|
||||
if (current.isViewRoot) {
|
||||
inheritedProtoView = new ProtoView(current.element, new ProtoRecordRange());
|
||||
inheritedProtoView = new ProtoView(current.element, new ProtoChangeDetector());
|
||||
if (isPresent(parent)) {
|
||||
if (isPresent(parent.inheritedElementBinder.nestedProtoView)) {
|
||||
throw new BaseException('Only one nested view per element is allowed');
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {DOM, Element, Node, Text, DocumentFragment, TemplateElement} from 'facade/dom';
|
||||
import {ListWrapper, MapWrapper, StringMapWrapper, List} from 'facade/collection';
|
||||
import {ProtoRecordRange, RecordRange, Record,
|
||||
ChangeDispatcher, AST, ContextWithVariableBindings} from 'change_detection/change_detection';
|
||||
import {AST, ContextWithVariableBindings, ChangeDispatcher, ProtoChangeDetector, ChangeDetector, ChangeRecord}
|
||||
from 'change_detection/change_detection';
|
||||
|
||||
import {ProtoElementInjector, ElementInjector, PreBuiltObjects} from './element_injector';
|
||||
import {ElementBinder} from './element_binder';
|
||||
|
@ -30,7 +30,7 @@ export class View {
|
|||
elementInjectors:List<ElementInjector>;
|
||||
bindElements:List<Element>;
|
||||
textNodes:List<Text>;
|
||||
recordRange:RecordRange;
|
||||
changeDetector:ChangeDetector;
|
||||
/// When the view is part of render tree, the DocumentFragment is empty, which is why we need
|
||||
/// to keep track of the nodes.
|
||||
nodes:List<Node>;
|
||||
|
@ -41,10 +41,10 @@ export class View {
|
|||
context: any;
|
||||
contextWithLocals:ContextWithVariableBindings;
|
||||
|
||||
constructor(proto:ProtoView, nodes:List<Node>, protoRecordRange:ProtoRecordRange, protoContextLocals:Map) {
|
||||
constructor(proto:ProtoView, nodes:List<Node>, protoChangeDetector:ProtoChangeDetector, protoContextLocals:Map) {
|
||||
this.proto = proto;
|
||||
this.nodes = nodes;
|
||||
this.recordRange = protoRecordRange.instantiate(this, NO_FORMATTERS);
|
||||
this.changeDetector = protoChangeDetector.instantiate(this, NO_FORMATTERS);
|
||||
this.elementInjectors = null;
|
||||
this.rootElementInjectors = null;
|
||||
this.textNodes = null;
|
||||
|
@ -92,7 +92,7 @@ export class View {
|
|||
// TODO(tbosch): if we have a contextWithLocals we actually only need to
|
||||
// set the contextWithLocals once. Would it be faster to always use a contextWithLocals
|
||||
// even if we don't have locals and not update the recordRange here?
|
||||
this.recordRange.setContext(this.context);
|
||||
this.changeDetector.setContext(this.context);
|
||||
}
|
||||
|
||||
_dehydrateContext() {
|
||||
|
@ -195,20 +195,20 @@ export class View {
|
|||
this._dehydrateContext();
|
||||
}
|
||||
|
||||
onRecordChange(groupMemento, records:List<Record>) {
|
||||
this._invokeMementoForRecords(records);
|
||||
onRecordChange(groupMemento, records:List) {
|
||||
this._invokeMementos(records);
|
||||
if (groupMemento instanceof DirectivePropertyGroupMemento) {
|
||||
this._notifyDirectiveAboutChanges(groupMemento, records);
|
||||
}
|
||||
}
|
||||
|
||||
_invokeMementoForRecords(records:List<Record>) {
|
||||
_invokeMementos(records:List) {
|
||||
for(var i = 0; i < records.length; ++i) {
|
||||
this._invokeMementoFor(records[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyDirectiveAboutChanges(groupMemento, records:List<Record>) {
|
||||
_notifyDirectiveAboutChanges(groupMemento, records:List) {
|
||||
var dir = groupMemento.directive(this.elementInjectors);
|
||||
if (dir instanceof OnChange) {
|
||||
dir.onChange(this._collectChanges(records));
|
||||
|
@ -216,8 +216,8 @@ export class View {
|
|||
}
|
||||
|
||||
// dispatch to element injector or text nodes based on context
|
||||
_invokeMementoFor(record:Record) {
|
||||
var memento = record.expressionMemento();
|
||||
_invokeMementoFor(record:ChangeRecord) {
|
||||
var memento = record.bindingMemento;
|
||||
if (memento instanceof DirectivePropertyMemento) {
|
||||
// we know that it is DirectivePropertyMemento
|
||||
var directiveMemento:DirectivePropertyMemento = memento;
|
||||
|
@ -234,12 +234,12 @@ export class View {
|
|||
}
|
||||
}
|
||||
|
||||
_collectChanges(records:List<Record>) {
|
||||
_collectChanges(records:List) {
|
||||
var changes = StringMapWrapper.create();
|
||||
for(var i = 0; i < records.length; ++i) {
|
||||
var record = records[i];
|
||||
var propertyUpdate = new PropertyUpdate(record.currentValue, record.previousValue);
|
||||
StringMapWrapper.set(changes, record.expressionMemento()._setterName, propertyUpdate);
|
||||
StringMapWrapper.set(changes, record.bindingMemento._setterName, propertyUpdate);
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
@ -248,7 +248,7 @@ export class View {
|
|||
export class ProtoView {
|
||||
element:Element;
|
||||
elementBinders:List<ElementBinder>;
|
||||
protoRecordRange:ProtoRecordRange;
|
||||
protoChangeDetector:ProtoChangeDetector;
|
||||
variableBindings: Map;
|
||||
protoContextLocals:Map;
|
||||
textNodesWithBindingCount:int;
|
||||
|
@ -258,12 +258,12 @@ export class ProtoView {
|
|||
isTemplateElement:boolean;
|
||||
constructor(
|
||||
template:Element,
|
||||
protoRecordRange:ProtoRecordRange) {
|
||||
protoChangeDetector:ProtoChangeDetector) {
|
||||
this.element = template;
|
||||
this.elementBinders = [];
|
||||
this.variableBindings = MapWrapper.create();
|
||||
this.protoContextLocals = MapWrapper.create();
|
||||
this.protoRecordRange = protoRecordRange;
|
||||
this.protoChangeDetector = protoChangeDetector;
|
||||
this.textNodesWithBindingCount = 0;
|
||||
this.elementsWithBindingCount = 0;
|
||||
this.instantiateInPlace = false;
|
||||
|
@ -299,8 +299,8 @@ export class ProtoView {
|
|||
} else {
|
||||
viewNodes = [rootElementClone];
|
||||
}
|
||||
var view = new View(this, viewNodes, this.protoRecordRange, this.protoContextLocals);
|
||||
|
||||
var view = new View(this, viewNodes, this.protoChangeDetector, this.protoContextLocals);
|
||||
var binders = this.elementBinders;
|
||||
var elementInjectors = ListWrapper.createFixedSize(binders.length);
|
||||
var rootElementInjectors = [];
|
||||
|
@ -353,7 +353,7 @@ export class ProtoView {
|
|||
var lightDom = null;
|
||||
if (isPresent(binder.componentDirective)) {
|
||||
var childView = binder.nestedProtoView.instantiate(elementInjector);
|
||||
view.recordRange.addRange(childView.recordRange);
|
||||
view.changeDetector.addChild(childView.changeDetector);
|
||||
|
||||
lightDom = binder.componentDirective.shadowDomStrategy.constructLightDom(view, childView, element);
|
||||
binder.componentDirective.shadowDomStrategy.attachTemplate(element, childView);
|
||||
|
@ -434,7 +434,7 @@ export class ProtoView {
|
|||
}
|
||||
ListWrapper.push(elBinder.textNodeIndices, indexInParent);
|
||||
var memento = this.textNodesWithBindingCount++;
|
||||
this.protoRecordRange.addRecordsFromAST(expression, memento, memento);
|
||||
this.protoChangeDetector.addAst(expression, memento, memento);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -447,7 +447,7 @@ export class ProtoView {
|
|||
this.elementsWithBindingCount++;
|
||||
}
|
||||
var memento = new ElementPropertyMemento(this.elementsWithBindingCount-1, setterName, setter);
|
||||
this.protoRecordRange.addRecordsFromAST(expression, memento, memento);
|
||||
this.protoChangeDetector.addAst(expression, memento, memento);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -478,7 +478,7 @@ export class ProtoView {
|
|||
setter
|
||||
);
|
||||
var groupMemento = DirectivePropertyGroupMemento.get(expMemento);
|
||||
this.protoRecordRange.addRecordsFromAST(expression, expMemento, groupMemento, isContentWatch);
|
||||
this.protoChangeDetector.addAst(expression, expMemento, groupMemento, isContentWatch);
|
||||
}
|
||||
|
||||
// Create a rootView as if the compiler encountered <rootcmp></rootcmp>,
|
||||
|
@ -487,7 +487,7 @@ export class ProtoView {
|
|||
static createRootProtoView(protoView: ProtoView,
|
||||
insertionElement, rootComponentAnnotatedType: DirectiveMetadata): ProtoView {
|
||||
DOM.addClass(insertionElement, 'ng-binding');
|
||||
var rootProtoView = new ProtoView(insertionElement, new ProtoRecordRange());
|
||||
var rootProtoView = new ProtoView(insertionElement, new ProtoChangeDetector());
|
||||
rootProtoView.instantiateInPlace = true;
|
||||
var binder = rootProtoView.bindElement(
|
||||
new ProtoElementInjector(null, 0, [rootComponentAnnotatedType.type], true));
|
||||
|
@ -507,7 +507,7 @@ export class ElementPropertyMemento {
|
|||
this._setter = setter;
|
||||
}
|
||||
|
||||
invoke(record:Record, bindElements:List<Element>) {
|
||||
invoke(record:ChangeRecord, bindElements:List<Element>) {
|
||||
var element:Element = bindElements[this._elementIndex];
|
||||
this._setter(element, record.currentValue);
|
||||
}
|
||||
|
@ -529,7 +529,7 @@ export class DirectivePropertyMemento {
|
|||
this._setter = setter;
|
||||
}
|
||||
|
||||
invoke(record:Record, elementInjectors:List<ElementInjector>) {
|
||||
invoke(record:ChangeRecord, elementInjectors:List<ElementInjector>) {
|
||||
var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex];
|
||||
var directive = elementInjector.getAtIndex(this._directiveIndex);
|
||||
this._setter(directive, record.currentValue);
|
||||
|
|
|
@ -84,7 +84,7 @@ export class ViewPort {
|
|||
} else {
|
||||
this._lightDom.redistribute();
|
||||
}
|
||||
this.parentView.recordRange.addRange(view.recordRange);
|
||||
this.parentView.changeDetector.addChild(view.changeDetector);
|
||||
this._linkElementInjectors(view);
|
||||
return view;
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ export class ViewPort {
|
|||
} else {
|
||||
this._lightDom.redistribute();
|
||||
}
|
||||
removedView.recordRange.remove();
|
||||
removedView.changeDetector.remove();
|
||||
this._unlinkElementInjectors(removedView);
|
||||
return removedView;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,11 @@ import {ListWrapper} from 'facade/collection';
|
|||
|
||||
export class LifeCycle {
|
||||
_changeDetector:ChangeDetector;
|
||||
_enforceNoNewChanges:boolean;
|
||||
|
||||
constructor(changeDetector:ChangeDetector) {
|
||||
constructor(changeDetector:ChangeDetector, enforceNoNewChanges:boolean = false) {
|
||||
this._changeDetector = changeDetector;
|
||||
this._enforceNoNewChanges = enforceNoNewChanges;
|
||||
}
|
||||
|
||||
registerWith(zone:VmTurnZone) {
|
||||
|
@ -26,5 +28,8 @@ export class LifeCycle {
|
|||
|
||||
tick() {
|
||||
this._changeDetector.detectChanges();
|
||||
if (this._enforceNoNewChanges) {
|
||||
this._changeDetector.checkNoChanges();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ export function main() {
|
|||
ctx = new MyComp();
|
||||
view = pv.instantiate(null);
|
||||
view.hydrate(new Injector([]), null, ctx);
|
||||
cd = new ChangeDetector(view.recordRange);
|
||||
cd = view.changeDetector;
|
||||
}
|
||||
|
||||
it('should consume text node changes', (done) => {
|
||||
|
|
|
@ -16,7 +16,7 @@ import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/
|
|||
import {ProtoElementInjector} from 'core/compiler/element_injector';
|
||||
import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader';
|
||||
|
||||
import {ChangeDetector, Lexer, Parser, ProtoRecordRange} from 'change_detection/change_detection';
|
||||
import {ChangeDetector, Lexer, Parser, ProtoChangeDetector} from 'change_detection/change_detection';
|
||||
import {Injector} from 'di/di';
|
||||
|
||||
export function main() {
|
||||
|
@ -65,7 +65,7 @@ export function main() {
|
|||
}
|
||||
if (isPresent(current.element.getAttribute('viewroot'))) {
|
||||
current.isViewRoot = true;
|
||||
current.inheritedProtoView = new ProtoView(current.element, new ProtoRecordRange());
|
||||
current.inheritedProtoView = new ProtoView(current.element, new ProtoChangeDetector());
|
||||
} else if (isPresent(parent)) {
|
||||
current.inheritedProtoView = parent.inheritedProtoView;
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ export function main() {
|
|||
evalContext = new Context();
|
||||
view = protoView.instantiate(null);
|
||||
view.hydrate(new Injector([]), null, evalContext);
|
||||
changeDetector = new ChangeDetector(view.recordRange);
|
||||
changeDetector = view.changeDetector;
|
||||
}
|
||||
|
||||
it('should not create an ElementBinder for elements that have no bindings', () => {
|
||||
|
@ -204,7 +204,7 @@ export function main() {
|
|||
var results = pipeline.process(el('<div viewroot prop-binding directives></div>'));
|
||||
var pv = results[0].inheritedProtoView;
|
||||
results[0].inheritedElementBinder.nestedProtoView = new ProtoView(
|
||||
el('<div></div>'), new ProtoRecordRange());
|
||||
el('<div></div>'), new ProtoChangeDetector());
|
||||
|
||||
instantiateView(pv);
|
||||
evalContext.prop1 = 'a';
|
||||
|
|
|
@ -34,7 +34,7 @@ export function main() {
|
|||
compiler.compile(MyComp, el(template)).
|
||||
then(createView).
|
||||
then((view) => {
|
||||
var lc = new LifeCycle(new ChangeDetector(view.recordRange));
|
||||
var lc = new LifeCycle(view.changeDetector, false);
|
||||
assertions(view, lc);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {ShadowDomEmulated} from 'core/compiler/shadow_dom';
|
|||
import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader';
|
||||
import {Component, Decorator, Template} from 'core/annotations/annotations';
|
||||
import {OnChange} from 'core/core';
|
||||
import {Lexer, Parser, ProtoRecordRange, ChangeDetector} from 'change_detection/change_detection';
|
||||
import {Lexer, Parser, ProtoChangeDetector, ChangeDetector} from 'change_detection/change_detection';
|
||||
import {TemplateConfig} from 'core/annotations/template_config';
|
||||
import {List, MapWrapper} from 'facade/collection';
|
||||
import {DOM, Element} from 'facade/dom';
|
||||
|
@ -48,10 +48,10 @@ export function main() {
|
|||
someTemplateDirective = new DirectiveMetadataReader().read(SomeTemplate);
|
||||
});
|
||||
|
||||
describe('instatiated from protoView', () => {
|
||||
describe('instantiated from protoView', () => {
|
||||
var view;
|
||||
beforeEach(() => {
|
||||
var pv = new ProtoView(el('<div id="1"></div>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(el('<div id="1"></div>'), new ProtoChangeDetector());
|
||||
view = pv.instantiate(null);
|
||||
});
|
||||
|
||||
|
@ -72,7 +72,7 @@ export function main() {
|
|||
describe('with locals', function() {
|
||||
var view;
|
||||
beforeEach(() => {
|
||||
var pv = new ProtoView(el('<div id="1"></div>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(el('<div id="1"></div>'), new ProtoChangeDetector());
|
||||
pv.bindVariable('context-foo', 'template-foo');
|
||||
view = createView(pv);
|
||||
});
|
||||
|
@ -108,7 +108,7 @@ export function main() {
|
|||
}
|
||||
|
||||
it('should collect the root node in the ProtoView element', () => {
|
||||
var pv = new ProtoView(templateAwareCreateElement('<div id="1"></div>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(templateAwareCreateElement('<div id="1"></div>'), new ProtoChangeDetector());
|
||||
var view = pv.instantiate(null);
|
||||
view.hydrate(null, null, null);
|
||||
expect(view.nodes.length).toBe(1);
|
||||
|
@ -118,7 +118,7 @@ export function main() {
|
|||
describe('collect elements with property bindings', () => {
|
||||
|
||||
it('should collect property bindings on the root element if it has the ng-binding class', () => {
|
||||
var pv = new ProtoView(templateAwareCreateElement('<div [prop]="a" class="ng-binding"></div>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(templateAwareCreateElement('<div [prop]="a" class="ng-binding"></div>'), new ProtoChangeDetector());
|
||||
pv.bindElement(null);
|
||||
pv.bindElementProperty(parser.parseBinding('a', null), 'prop', reflector.setter('prop'));
|
||||
|
||||
|
@ -130,7 +130,7 @@ export function main() {
|
|||
|
||||
it('should collect property bindings on child elements with ng-binding class', () => {
|
||||
var pv = new ProtoView(templateAwareCreateElement('<div><span></span><span class="ng-binding"></span></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(null);
|
||||
pv.bindElementProperty(parser.parseBinding('b', null), 'a', reflector.setter('a'));
|
||||
|
||||
|
@ -145,7 +145,7 @@ export function main() {
|
|||
describe('collect text nodes with bindings', () => {
|
||||
|
||||
it('should collect text nodes under the root element', () => {
|
||||
var pv = new ProtoView(templateAwareCreateElement('<div class="ng-binding">{{}}<span></span>{{}}</div>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(templateAwareCreateElement('<div class="ng-binding">{{}}<span></span>{{}}</div>'), new ProtoChangeDetector());
|
||||
pv.bindElement(null);
|
||||
pv.bindTextNode(0, parser.parseBinding('a', null));
|
||||
pv.bindTextNode(2, parser.parseBinding('b', null));
|
||||
|
@ -159,7 +159,7 @@ export function main() {
|
|||
|
||||
it('should collect text nodes with bindings on child elements with ng-binding class', () => {
|
||||
var pv = new ProtoView(templateAwareCreateElement('<div><span> </span><span class="ng-binding">{{}}</span></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(null);
|
||||
pv.bindTextNode(0, parser.parseBinding('b', null));
|
||||
|
||||
|
@ -175,7 +175,7 @@ export function main() {
|
|||
describe('inplace instantiation', () => {
|
||||
it('should be supported.', () => {
|
||||
var template = el('<div></div>');
|
||||
var pv = new ProtoView(template, new ProtoRecordRange());
|
||||
var pv = new ProtoView(template, new ProtoChangeDetector());
|
||||
pv.instantiateInPlace = true;
|
||||
var view = pv.instantiate(null);
|
||||
view.hydrate(null, null, null);
|
||||
|
@ -184,7 +184,7 @@ export function main() {
|
|||
|
||||
it('should be off by default.', () => {
|
||||
var template = el('<div></div>')
|
||||
var view = new ProtoView(template, new ProtoRecordRange())
|
||||
var view = new ProtoView(template, new ProtoChangeDetector())
|
||||
.instantiate(null);
|
||||
view.hydrate(null, null, null);
|
||||
expect(view.nodes[0]).not.toBe(template);
|
||||
|
@ -201,7 +201,7 @@ export function main() {
|
|||
|
||||
describe('create ElementInjectors', () => {
|
||||
it('should use the directives of the ProtoElementInjector', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"></div>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(el('<div class="ng-binding"></div>'), new ProtoChangeDetector());
|
||||
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
|
||||
|
||||
var view = pv.instantiate(null);
|
||||
|
@ -212,7 +212,7 @@ export function main() {
|
|||
|
||||
it('should use the correct parent', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"><span class="ng-binding"></span></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]);
|
||||
pv.bindElement(protoParent);
|
||||
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
|
||||
|
@ -226,7 +226,7 @@ export function main() {
|
|||
|
||||
it('should not pass the host injector when a parent injector exists', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"><span class="ng-binding"></span></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]);
|
||||
pv.bindElement(protoParent);
|
||||
var testProtoElementInjector = new TestProtoElementInjector(protoParent, 1, [AnotherDirective]);
|
||||
|
@ -242,7 +242,7 @@ export function main() {
|
|||
|
||||
it('should pass the host injector when there is no parent injector', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"><span class="ng-binding"></span></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective]));
|
||||
var testProtoElementInjector = new TestProtoElementInjector(null, 1, [AnotherDirective]);
|
||||
pv.bindElement(testProtoElementInjector);
|
||||
|
@ -259,7 +259,7 @@ export function main() {
|
|||
|
||||
it('should collect a single root element injector', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"><span class="ng-binding"></span></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
var protoParent = new ProtoElementInjector(null, 0, [SomeDirective]);
|
||||
pv.bindElement(protoParent);
|
||||
pv.bindElement(new ProtoElementInjector(protoParent, 1, [AnotherDirective]));
|
||||
|
@ -272,7 +272,7 @@ export function main() {
|
|||
|
||||
it('should collect multiple root element injectors', () => {
|
||||
var pv = new ProtoView(el('<div><span class="ng-binding"></span><span class="ng-binding"></span></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
|
||||
pv.bindElement(new ProtoElementInjector(null, 2, [AnotherDirective]));
|
||||
|
||||
|
@ -289,7 +289,7 @@ export function main() {
|
|||
var ctx;
|
||||
|
||||
function createComponentWithSubPV(subProtoView) {
|
||||
var pv = new ProtoView(el('<cmp class="ng-binding"></cmp>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(el('<cmp class="ng-binding"></cmp>'), new ProtoChangeDetector());
|
||||
var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponent], true));
|
||||
binder.componentDirective = someComponentDirective;
|
||||
binder.nestedProtoView = subProtoView;
|
||||
|
@ -304,7 +304,7 @@ export function main() {
|
|||
}
|
||||
|
||||
it('should expose component services to the component', () => {
|
||||
var subpv = new ProtoView(el('<span></span>'), new ProtoRecordRange());
|
||||
var subpv = new ProtoView(el('<span></span>'), new ProtoChangeDetector());
|
||||
var pv = createComponentWithSubPV(subpv);
|
||||
|
||||
var view = createNestedView(pv);
|
||||
|
@ -316,7 +316,7 @@ export function main() {
|
|||
it('should expose component services and component instance to directives in the shadow Dom',
|
||||
() => {
|
||||
var subpv = new ProtoView(
|
||||
el('<div dec class="ng-binding">hello shadow dom</div>'), new ProtoRecordRange());
|
||||
el('<div dec class="ng-binding">hello shadow dom</div>'), new ProtoChangeDetector());
|
||||
subpv.bindElement(
|
||||
new ProtoElementInjector(null, 0, [ServiceDependentDecorator]));
|
||||
var pv = createComponentWithSubPV(subpv);
|
||||
|
@ -339,7 +339,7 @@ export function main() {
|
|||
|
||||
it('dehydration should dehydrate child component views too', () => {
|
||||
var subpv = new ProtoView(
|
||||
el('<div dec class="ng-binding">hello shadow dom</div>'), new ProtoRecordRange());
|
||||
el('<div dec class="ng-binding">hello shadow dom</div>'), new ProtoChangeDetector());
|
||||
subpv.bindElement(
|
||||
new ProtoElementInjector(null, 0, [ServiceDependentDecorator]));
|
||||
var pv = createComponentWithSubPV(subpv);
|
||||
|
@ -354,7 +354,7 @@ export function main() {
|
|||
});
|
||||
|
||||
it('should create shadow dom', () => {
|
||||
var subpv = new ProtoView(el('<span>hello shadow dom</span>'), new ProtoRecordRange());
|
||||
var subpv = new ProtoView(el('<span>hello shadow dom</span>'), new ProtoChangeDetector());
|
||||
var pv = createComponentWithSubPV(subpv);
|
||||
|
||||
var view = createNestedView(pv);
|
||||
|
@ -363,9 +363,9 @@ export function main() {
|
|||
});
|
||||
|
||||
it('should use the provided shadow DOM strategy', () => {
|
||||
var subpv = new ProtoView(el('<span>hello shadow dom</span>'), new ProtoRecordRange());
|
||||
var subpv = new ProtoView(el('<span>hello shadow dom</span>'), new ProtoChangeDetector());
|
||||
|
||||
var pv = new ProtoView(el('<cmp class="ng-binding"></cmp>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(el('<cmp class="ng-binding"></cmp>'), new ProtoChangeDetector());
|
||||
var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeComponentWithEmulatedShadowDom], true));
|
||||
binder.componentDirective = new DirectiveMetadataReader().read(SomeComponentWithEmulatedShadowDom);
|
||||
binder.nestedProtoView = subpv;
|
||||
|
@ -379,8 +379,8 @@ export function main() {
|
|||
describe('with template views', () => {
|
||||
function createViewWithTemplate() {
|
||||
var templateProtoView = new ProtoView(
|
||||
el('<div id="1"></div>'), new ProtoRecordRange());
|
||||
var pv = new ProtoView(el('<someTmpl class="ng-binding"></someTmpl>'), new ProtoRecordRange());
|
||||
el('<div id="1"></div>'), new ProtoChangeDetector());
|
||||
var pv = new ProtoView(el('<someTmpl class="ng-binding"></someTmpl>'), new ProtoChangeDetector());
|
||||
var binder = pv.bindElement(new ProtoElementInjector(null, 0, [SomeTemplate]));
|
||||
binder.templateDirective = someTemplateDirective;
|
||||
binder.nestedProtoView = templateProtoView;
|
||||
|
@ -424,7 +424,7 @@ export function main() {
|
|||
|
||||
function createProtoView() {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"><div></div></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(new TestProtoElementInjector(null, 0, []));
|
||||
pv.bindEvent('click', parser.parseBinding('callMe(\$event)', null));
|
||||
return pv;
|
||||
|
@ -464,12 +464,12 @@ export function main() {
|
|||
function createViewAndChangeDetector(protoView) {
|
||||
view = createView(protoView);
|
||||
ctx = view.context;
|
||||
cd = new ChangeDetector(view.recordRange);
|
||||
cd = view.changeDetector;
|
||||
}
|
||||
|
||||
it('should consume text node changes', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding">{{}}</div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(null);
|
||||
pv.bindTextNode(0, parser.parseBinding('foo', null));
|
||||
createViewAndChangeDetector(pv);
|
||||
|
@ -481,7 +481,7 @@ export function main() {
|
|||
|
||||
it('should consume element binding changes', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(null);
|
||||
pv.bindElementProperty(parser.parseBinding('foo', null), 'id', reflector.setter('id'));
|
||||
createViewAndChangeDetector(pv);
|
||||
|
@ -493,7 +493,7 @@ export function main() {
|
|||
|
||||
it('should consume directive watch expression change', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective]));
|
||||
pv.bindDirectiveProperty(0, parser.parseBinding('foo', null), 'prop', reflector.setter('prop'), false);
|
||||
createViewAndChangeDetector(pv);
|
||||
|
@ -505,7 +505,7 @@ export function main() {
|
|||
|
||||
it('should notify a directive about changes after all its properties have been set', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
|
||||
pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange]));
|
||||
pv.bindDirectiveProperty( 0, parser.parseBinding('a', null), 'a', reflector.setter('a'), false);
|
||||
|
@ -522,7 +522,7 @@ export function main() {
|
|||
|
||||
it('should provide a map of updated properties', () => {
|
||||
var pv = new ProtoView(el('<div class="ng-binding"></div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
|
||||
pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange]));
|
||||
pv.bindDirectiveProperty( 0, parser.parseBinding('a', null), 'a', reflector.setter('a'), false);
|
||||
|
@ -547,7 +547,7 @@ export function main() {
|
|||
var element, pv;
|
||||
beforeEach(() => {
|
||||
element = DOM.createElement('div');
|
||||
pv = new ProtoView(el('<div>hi</div>'), new ProtoRecordRange());
|
||||
pv = new ProtoView(el('<div>hi</div>'), new ProtoChangeDetector());
|
||||
});
|
||||
|
||||
it('should create the root component when instantiated', () => {
|
||||
|
|
|
@ -5,10 +5,10 @@ import {DOM} from 'facade/dom';
|
|||
import {ListWrapper, MapWrapper} from 'facade/collection';
|
||||
import {Injector} from 'di/di';
|
||||
import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector';
|
||||
import {ProtoRecordRange, Lexer, Parser} from 'change_detection/change_detection';
|
||||
import {ProtoChangeDetector, Lexer, Parser} from 'change_detection/change_detection';
|
||||
|
||||
function createView(nodes) {
|
||||
var view = new View(null, nodes, new ProtoRecordRange(), MapWrapper.create());
|
||||
var view = new View(null, nodes, new ProtoChangeDetector(), MapWrapper.create());
|
||||
view.init([], [], [], [], [], [], []);
|
||||
return view;
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export function main() {
|
|||
dom = el(`<div><stuff></stuff><div insert-after-me></div><stuff></stuff></div>`);
|
||||
var insertionElement = dom.childNodes[1];
|
||||
parentView = createView([dom.childNodes[0]]);
|
||||
protoView = new ProtoView(el('<div>hi</div>'), new ProtoRecordRange());
|
||||
protoView = new ProtoView(el('<div>hi</div>'), new ProtoChangeDetector());
|
||||
elementInjector = new ElementInjector(null, null, null);
|
||||
viewPort = new ViewPort(parentView, insertionElement, protoView, elementInjector);
|
||||
customViewWithOneNode = createView([el('<div>single</div>')]);
|
||||
|
@ -117,25 +117,26 @@ export function main() {
|
|||
viewPort.hydrate(new Injector([]), null);
|
||||
|
||||
var pv = new ProtoView(el('<div class="ng-binding">{{}}</div>'),
|
||||
new ProtoRecordRange());
|
||||
new ProtoChangeDetector());
|
||||
pv.bindElement(new ProtoElementInjector(null, 1, [SomeDirective]));
|
||||
pv.bindTextNode(0, parser.parseBinding('foo', null));
|
||||
fancyView = pv.instantiate(null);
|
||||
});
|
||||
|
||||
it('hydrating should update rootElementInjectors and parent RR', () => {
|
||||
it('hydrating should update rootElementInjectors and parent change detector', () => {
|
||||
viewPort.insert(fancyView);
|
||||
ListWrapper.forEach(fancyView.rootElementInjectors, (inj) =>
|
||||
expect(inj.parent).toBe(elementInjector));
|
||||
expect(parentView.recordRange.findFirstEnabledRecord()).not.toBe(null);
|
||||
|
||||
expect(parentView.changeDetector.children.length).toBe(1);
|
||||
});
|
||||
|
||||
it('dehydrating should update rootElementInjectors and parent RR', () => {
|
||||
it('dehydrating should update rootElementInjectors and parent change detector', () => {
|
||||
viewPort.insert(fancyView);
|
||||
viewPort.remove();
|
||||
ListWrapper.forEach(fancyView.rootElementInjectors, (inj) =>
|
||||
expect(inj.parent).toBe(null));
|
||||
expect(parentView.recordRange.findFirstEnabledRecord()).toBe(null);
|
||||
expect(parentView.changeDetector.children.length).toBe(0);
|
||||
expect(viewPort.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ export function main() {
|
|||
component = new TestComponent();
|
||||
view = pv.instantiate(null);
|
||||
view.hydrate(new Injector([]), null, component);
|
||||
cd = new ChangeDetector(view.recordRange);
|
||||
cd = view.changeDetector;
|
||||
}
|
||||
|
||||
function compileWithTemplate(template) {
|
||||
|
|
|
@ -27,7 +27,7 @@ export function main() {
|
|||
component = new TestComponent();
|
||||
view = pv.instantiate(null);
|
||||
view.hydrate(new Injector([]), null, component);
|
||||
cd = new ChangeDetector(view.recordRange);
|
||||
cd = view.changeDetector;
|
||||
}
|
||||
|
||||
function compileWithTemplate(template) {
|
||||
|
|
|
@ -101,6 +101,9 @@ class ListWrapper {
|
|||
list.remove(items[i]);
|
||||
}
|
||||
}
|
||||
static remove(List list, item) {
|
||||
list.remove(item);
|
||||
}
|
||||
static void clear(List l) { l.clear(); }
|
||||
static String join(List l, String s) => l.join(s);
|
||||
static bool isEmpty(list) => list.isEmpty;
|
||||
|
|
|
@ -149,6 +149,10 @@ export class ListWrapper {
|
|||
list.splice(index, 1);
|
||||
}
|
||||
}
|
||||
static remove(list, item) {
|
||||
var index = list.indexOf(item);
|
||||
list.splice(index, 1);
|
||||
}
|
||||
static clear(list) {
|
||||
list.splice(0, list.length);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue