feat(change_detection): add support for pipes

This commit is contained in:
vsavkin
2015-02-12 14:56:41 -08:00
parent fa25965939
commit 695b4ebbc7
20 changed files with 436 additions and 257 deletions

View File

@ -95,6 +95,7 @@ var PROTOS_ACCESSOR = "this.protos";
var CHANGE_LOCAL = "change";
var CHANGES_LOCAL = "changes";
var TEMP_LOCAL = "temp";
var PIPE_REGISTRY_ACCESSOR = "this.pipeRegistry";
function typeTemplate(type:string, cons:string, detectChanges:string, setContext:string):string {
return `
@ -102,18 +103,19 @@ ${cons}
${detectChanges}
${setContext};
return function(dispatcher, formatters) {
return new ${type}(dispatcher, formatters, protos);
return function(dispatcher, formatters, pipeRegistry) {
return new ${type}(dispatcher, formatters, pipeRegistry, protos);
}
`;
}
function constructorTemplate(type:string, fieldsDefinitions:string):string {
return `
var ${type} = function ${type}(dispatcher, formatters, protos) {
var ${type} = function ${type}(dispatcher, formatters, pipeRegistry, protos) {
${ABSTRACT_CHANGE_DETECTOR}.call(this);
${DISPATCHER_ACCESSOR} = dispatcher;
${FORMATTERS_ACCESSOR} = formatters;
${PIPE_REGISTRY_ACCESSOR} = pipeRegistry;
${PROTOS_ACCESSOR} = protos;
${fieldsDefinitions}
}
@ -162,14 +164,18 @@ if (${CHANGES_LOCAL} && ${CHANGES_LOCAL}.length > 0) {
`;
}
function structuralCheckTemplate(selfIndex:number, field:string, context:string, notify:string):string{
function pipeCheckTemplate(context:string, pipe:string,
value:string, change:string, addRecord:string, notify:string):string{
return `
${CHANGE_LOCAL} = ${UTIL}.structuralCheck(${field}, ${context});
if (${CHANGE_LOCAL}) {
${CHANGES_LOCAL} = ${UTIL}.addRecord(${CHANGES_LOCAL},
${UTIL}.changeRecord(${PROTOS_ACCESSOR}[${selfIndex}].bindingMemento, ${CHANGE_LOCAL}));
${field} = ${CHANGE_LOCAL}.currentValue;
if (${pipe} === ${UTIL}.unitialized() || !${pipe}.supports(${context})) {
${pipe} = ${PIPE_REGISTRY_ACCESSOR}.get('[]', ${context});
}
${CHANGE_LOCAL} = ${pipe}.transform(${context});
if (! ${UTIL}.noChangeMarker(${CHANGE_LOCAL})) {
${value} = ${CHANGE_LOCAL};
${change} = true;
${addRecord}
}
${notify}
`;
@ -235,6 +241,7 @@ export class ChangeDetectorJITGenerator {
localNames:List<String>;
changeNames:List<String>;
fieldNames:List<String>;
pipeNames:List<String>;
constructor(typeName:string, records:List<ProtoRecord>) {
this.typeName = typeName;
@ -243,6 +250,7 @@ export class ChangeDetectorJITGenerator {
this.localNames = this.getLocalNames(records);
this.changeNames = this.getChangeNames(this.localNames);
this.fieldNames = this.getFieldNames(this.localNames);
this.pipeNames = this.getPipeNames(this.localNames);
}
getLocalNames(records:List<ProtoRecord>):List<String> {
@ -262,6 +270,9 @@ export class ChangeDetectorJITGenerator {
return localNames.map((n) => `this.${n}`);
}
getPipeNames(localNames:List<String>):List<String> {
return localNames.map((n) => `this.${n}_pipe`);
}
generate():Function {
var text = typeTemplate(this.typeName, this.genConstructor(), this.genDetectChanges(), this.genSetContext());
@ -269,7 +280,16 @@ export class ChangeDetectorJITGenerator {
}
genConstructor():string {
return constructorTemplate(this.typeName, fieldDefinitionsTemplate(this.fieldNames));
var fields = [];
fields = fields.concat(this.fieldNames);
this.records.forEach((r) => {
if (r.mode === RECORD_TYPE_STRUCTURAL_CHECK) {
fields.push(this.pipeNames[r.selfIndex]);
}
});
return constructorTemplate(this.typeName, fieldDefinitionsTemplate(fields));
}
genSetContext():string {
@ -295,17 +315,24 @@ export class ChangeDetectorJITGenerator {
}
genRecord(r:ProtoRecord):string {
if (r.mode == RECORD_TYPE_STRUCTURAL_CHECK) {
return this.getStructuralCheck(r);
if (r.mode === RECORD_TYPE_STRUCTURAL_CHECK) {
return this.genPipeCheck (r);
} else {
return this.genReferenceCheck(r);
}
}
getStructuralCheck(r:ProtoRecord):string {
var field = this.fieldNames[r.selfIndex];
genPipeCheck(r:ProtoRecord):string {
var context = this.localNames[r.contextIndex];
return structuralCheckTemplate(r.selfIndex - 1, field, context, this.genNotify(r));
var pipe = this.pipeNames[r.selfIndex];
var newValue = this.localNames[r.selfIndex];
var oldValue = this.fieldNames[r.selfIndex];
var change = this.changeNames[r.selfIndex];
var addRecord = addSimpleChangeRecordTemplate(r.selfIndex - 1, oldValue, newValue);
var notify = this.genNotify(r);
return pipeCheckTemplate(context, pipe, newValue, change, addRecord, notify);
}
genReferenceCheck(r:ProtoRecord):string {

View File

@ -1,10 +1,9 @@
import {isPresent, isBlank, BaseException, Type} from 'angular2/src/facade/lang';
import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
import {ArrayChanges} from './array_changes';
import {KeyValueChanges} from './keyvalue_changes';
import {ProtoRecord} from './proto_change_detector';
import {ExpressionChangedAfterItHasBeenChecked} from './exceptions';
import {NO_CHANGE} from './pipes/pipe';
import {ChangeRecord, ChangeDetector, CHECK_ALWAYS, CHECK_ONCE, CHECKED, DETACHED} from './interfaces';
export var uninitialized = new Object();
@ -85,10 +84,6 @@ function _changeRecord(bindingMemento, change) {
var _singleElementList = [null];
function _isBlank(val):boolean {
return isBlank(val) || val === uninitialized;
}
export class ChangeDetectionUtil {
static unitialized() {
return uninitialized;
@ -149,32 +144,6 @@ export class ChangeDetectionUtil {
return obj[args[0]];
}
static structuralCheck(self, context) {
if (_isBlank(self) && _isBlank(context)) {
return null;
} else if (_isBlank(context)) {
return new SimpleChange(null, null);
}
if (_isBlank(self)) {
if (ArrayChanges.supports(context)) {
self = new ArrayChanges();
} else if (KeyValueChanges.supports(context)) {
self = new KeyValueChanges();
}
}
if (isBlank(self) || !self.supportsObj(context)) {
throw new BaseException(`Unsupported type (${context})`);
}
if (self.check(context)) {
return new SimpleChange(null, self); // TODO: don't wrap and return self instead
} else {
return null;
}
}
static findContext(name:string, c){
while (c instanceof ContextWithVariableBindings) {
if (c.hasBinding(name)) {
@ -185,6 +154,10 @@ export class ChangeDetectionUtil {
return c;
}
static noChangeMarker(value):boolean {
return value === NO_CHANGE;
}
static throwOnChange(proto:ProtoRecord, change) {
throw new ExpressionChangedAfterItHasBeenChecked(proto, change);
}

View File

@ -3,6 +3,7 @@ import {List, ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/faca
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
import {AbstractChangeDetector} from './abstract_change_detector';
import {PipeRegistry} from './pipes/pipe_registry';
import {ChangeDetectionUtil, SimpleChange, uninitialized} from './change_detection_util';
@ -26,16 +27,24 @@ import {ExpressionChangedAfterItHasBeenChecked, ChangeDetectionError} from './ex
export class DynamicChangeDetector extends AbstractChangeDetector {
dispatcher:any;
formatters:Map;
pipeRegistry;
values:List;
changes:List;
pipes:List;
prevContexts:List;
protos:List<ProtoRecord>;
constructor(dispatcher:any, formatters:Map, protoRecords:List<ProtoRecord>) {
constructor(dispatcher:any, formatters:Map, pipeRegistry:PipeRegistry, protoRecords:List<ProtoRecord>) {
super();
this.dispatcher = dispatcher;
this.formatters = formatters;
this.pipeRegistry = pipeRegistry;
this.values = ListWrapper.createFixedSize(protoRecords.length + 1);
this.pipes = ListWrapper.createFixedSize(protoRecords.length + 1);
this.prevContexts = ListWrapper.createFixedSize(protoRecords.length + 1);
this.changes = ListWrapper.createFixedSize(protoRecords.length + 1);
this.protos = protoRecords;
@ -43,6 +52,9 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
setContext(context:any) {
ListWrapper.fill(this.values, uninitialized);
ListWrapper.fill(this.changes, false);
ListWrapper.fill(this.pipes, null);
ListWrapper.fill(this.prevContexts, uninitialized);
this.values[0] = context;
}
@ -71,7 +83,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
_check(proto:ProtoRecord) {
try {
if (proto.mode == RECORD_TYPE_STRUCTURAL_CHECK) {
return this._structuralCheck(proto);
return this._pipeCheck(proto);
} else {
return this._referenceCheck(proto);
}
@ -147,15 +159,36 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
}
}
_structuralCheck(proto:ProtoRecord) {
var self = this._readSelf(proto);
_pipeCheck(proto:ProtoRecord) {
var context = this._readContext(proto);
var pipe = this._pipeFor(proto, context);
var change = ChangeDetectionUtil.structuralCheck(self, context);
if (isPresent(change)) {
this._writeSelf(proto, change.currentValue);
var newValue = pipe.transform(context);
if (! ChangeDetectionUtil.noChangeMarker(newValue)) {
this._writeSelf(proto, newValue);
this._setChanged(proto, true);
if (proto.lastInBinding) {
var prevValue = this._readSelf(proto);
return ChangeDetectionUtil.simpleChange(prevValue, newValue);
} else {
return null;
}
} else {
this._setChanged(proto, false);
return null;
}
}
_pipeFor(proto:ProtoRecord, context) {
var storedPipe = this._readPipe(proto);
if (isPresent(storedPipe) && storedPipe.supports(context)) {
return storedPipe;
} else {
var pipe = this.pipeRegistry.get("[]", context);
this._writePipe(proto, pipe);
return pipe;
}
return change;
}
_readContext(proto:ProtoRecord) {
@ -170,6 +203,14 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
this.values[proto.selfIndex] = value;
}
_readPipe(proto:ProtoRecord) {
return this.pipes[proto.selfIndex];
}
_writePipe(proto:ProtoRecord, value) {
this.pipes[proto.selfIndex] = value;
}
_setChanged(proto:ProtoRecord, value:boolean) {
this.changes[proto.selfIndex] = value;
}

View File

@ -14,7 +14,9 @@ import {
looseIdentical,
} from 'angular2/src/facade/lang';
export class ArrayChanges {
import {NO_CHANGE, Pipe} from './pipe';
export class ArrayChanges extends Pipe {
_collection;
_length:int;
_linkedRecords:_DuplicateMap;
@ -30,6 +32,7 @@ export class ArrayChanges {
_removalsTail:CollectionChangeRecord;
constructor() {
super();
this._collection = null;
this._length = null;
/// Keeps track of the used records at any point in time (during & across `_check()` calls)
@ -48,12 +51,12 @@ export class ArrayChanges {
this._removalsTail = null;
}
static supports(obj):boolean {
static supportsObj(obj):boolean {
return isListLikeIterable(obj);
}
supportsObj(obj):boolean {
return ArrayChanges.supports(obj);
supports(obj):boolean {
return ArrayChanges.supportsObj(obj);
}
get collection() {
@ -99,6 +102,14 @@ export class ArrayChanges {
}
}
transform(collection){
if (this.check(collection)) {
return this;
} else {
return NO_CHANGE;
}
}
// todo(vicb): optim for UnmodifiableListView (frozen arrays)
check(collection):boolean {
this._reset();

View File

@ -0,0 +1,27 @@
import {isBlank} from 'angular2/src/facade/lang';
import {Pipe, NO_CHANGE} from './pipe';
export class NullPipe extends Pipe {
called:boolean;
constructor() {
super();
this.called = false;
}
static supportsObj(obj):boolean {
return isBlank(obj);
}
supports(obj) {
return NullPipe.supportsObj(obj);
}
transform(value) {
if (! this.called) {
this.called = true;
return null;
} else {
return NO_CHANGE;
}
}
}

View File

@ -0,0 +1,6 @@
export var NO_CHANGE = new Object();
export class Pipe {
supports(obj):boolean {return false;}
transform(value:any):any {return null;}
}

View File

@ -0,0 +1,27 @@
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {isBlank, isPresent, BaseException, CONST} from 'angular2/src/facade/lang';
import {Pipe} from './pipe';
export class PipeRegistry {
config;
constructor(config){
this.config = config;
}
get(type:string, obj):Pipe {
var listOfConfigs = this.config[type];
if (isBlank(listOfConfigs)) {
throw new BaseException(`Cannot find a pipe for type '${type}' object '${obj}'`);
}
var matchingConfig = ListWrapper.find(listOfConfigs,
(pipeConfig) => pipeConfig["supports"](obj));
if (isBlank(matchingConfig)) {
throw new BaseException(`Cannot find a pipe for type '${type}' object '${obj}'`);
}
return matchingConfig["pipe"]();
}
}

View File

@ -27,6 +27,7 @@ import {ChangeRecord, ChangeDispatcher, ChangeDetector} from './interfaces';
import {ChangeDetectionUtil} from './change_detection_util';
import {DynamicChangeDetector} from './dynamic_change_detector';
import {ChangeDetectorJITGenerator} from './change_detection_jit_generator';
import {PipeRegistry} from './pipes/pipe_registry';
import {coalesce} from './coalesce';
@ -89,6 +90,7 @@ export class ProtoRecord {
}
}
export class ProtoChangeDetector {
addAst(ast:AST, bindingMemento:any, directiveMemento:any = null, structural:boolean = false){}
instantiate(dispatcher:any, formatters:Map):ChangeDetector{
@ -99,9 +101,11 @@ export class ProtoChangeDetector {
export class DynamicProtoChangeDetector extends ProtoChangeDetector {
_records:List<ProtoRecord>;
_recordBuilder:ProtoRecordBuilder;
_pipeRegistry:PipeRegistry;
constructor() {
constructor(pipeRegistry:PipeRegistry) {
super();
this._pipeRegistry = pipeRegistry;
this._records = null;
this._recordBuilder = new ProtoRecordBuilder();
}
@ -112,7 +116,8 @@ export class DynamicProtoChangeDetector extends ProtoChangeDetector {
instantiate(dispatcher:any, formatters:Map) {
this._createRecordsIfNecessary();
return new DynamicChangeDetector(dispatcher, formatters, this._records);
return new DynamicChangeDetector(dispatcher, formatters,
this._pipeRegistry, this._records);
}
_createRecordsIfNecessary() {
@ -127,9 +132,11 @@ var _jitProtoChangeDetectorClassCounter:number = 0;
export class JitProtoChangeDetector extends ProtoChangeDetector {
_factory:Function;
_recordBuilder:ProtoRecordBuilder;
_pipeRegistry;
constructor() {
constructor(pipeRegistry) {
super();
this._pipeRegistry = pipeRegistry;
this._factory = null;
this._recordBuilder = new ProtoRecordBuilder();
}
@ -140,7 +147,7 @@ export class JitProtoChangeDetector extends ProtoChangeDetector {
instantiate(dispatcher:any, formatters:Map) {
this._createFactoryIfNecessary();
return this._factory(dispatcher, formatters);
return this._factory(dispatcher, formatters, this._pipeRegistry);
}
_createFactoryIfNecessary() {

View File

@ -1,5 +1,4 @@
import {Viewport, onChange} from 'angular2/src/core/annotations/annotations';
import {OnChange} from 'angular2/src/core/compiler/interfaces';
import {Viewport} from 'angular2/src/core/annotations/annotations';
import {ViewContainer} from 'angular2/src/core/compiler/view_container';
import {View} from 'angular2/src/core/compiler/view';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
@ -7,21 +6,19 @@ import {ListWrapper} from 'angular2/src/facade/collection';
@Viewport({
selector: '[foreach][in]',
lifecycle: [onChange],
bind: {
'in': 'iterable[]'
'in': 'iterableChanges[]'
}
})
export class Foreach extends OnChange {
export class Foreach {
viewContainer: ViewContainer;
iterable;
constructor(viewContainer: ViewContainer) {
constructor(viewContainer:ViewContainer) {
super();
this.viewContainer = viewContainer;
}
onChange(changes) {
var iteratorChanges = changes['iterable'];
if (isBlank(iteratorChanges) || isBlank(iteratorChanges.currentValue)) {
set iterableChanges(changes) {
if (isBlank(changes)) {
this.viewContainer.clear();
return;
}
@ -29,17 +26,17 @@ export class Foreach extends OnChange {
// TODO(rado): check if change detection can produce a change record that is
// easier to consume than current.
var recordViewTuples = [];
iteratorChanges.currentValue.forEachRemovedItem(
changes.forEachRemovedItem(
(removedRecord) => ListWrapper.push(recordViewTuples, new RecordViewTuple(removedRecord, null))
);
iteratorChanges.currentValue.forEachMovedItem(
changes.forEachMovedItem(
(movedRecord) => ListWrapper.push(recordViewTuples, new RecordViewTuple(movedRecord, null))
);
var insertTuples = Foreach.bulkRemove(recordViewTuples, this.viewContainer);
iteratorChanges.currentValue.forEachAddedItem(
changes.forEachAddedItem(
(addedRecord) => ListWrapper.push(insertTuples, new RecordViewTuple(addedRecord, null))
);