feat(change_detector): notify directives on property changes
This commit is contained in:
		
							parent
							
								
									5bdefee6c9
								
							
						
					
					
						commit
						847cefcb7b
					
				| @ -1,25 +1,70 @@ | ||||
| import {ProtoRecordRange, RecordRange} from './record_range'; | ||||
| import {ProtoRecord, Record} from './record'; | ||||
| import {FIELD, int, isPresent} from 'facade/lang'; | ||||
| import {int, isPresent, isBlank} from 'facade/lang'; | ||||
| import {ListWrapper, List} from 'facade/collection'; | ||||
| 
 | ||||
| export * from './record'; | ||||
| export * from './record_range' | ||||
| 
 | ||||
| export class ChangeDetector { | ||||
|   _rootRecordRange:RecordRange; | ||||
| 
 | ||||
|   constructor(recordRange:RecordRange) { | ||||
|     this._rootRecordRange = recordRange; | ||||
|   } | ||||
| 
 | ||||
|   detectChanges():int { | ||||
|     var count:int = 0; | ||||
|     for (var record = this._rootRecordRange.findFirstEnabledRecord(); | ||||
|          isPresent(record); | ||||
|          record = record.nextEnabled) { | ||||
|     var count = 0; | ||||
|     var updatedRecords = null; | ||||
|     var record = this._rootRecordRange.findFirstEnabledRecord(); | ||||
| 
 | ||||
|     while (isPresent(record)) { | ||||
|       var currentRange = record.recordRange; | ||||
|       var currentGroup = record.groupMemento(); | ||||
| 
 | ||||
|       var nextEnabled = record.nextEnabled; | ||||
|       var nextRange = isPresent(nextEnabled) ? nextEnabled.recordRange : null; | ||||
|       var nextGroup = isPresent(nextEnabled) ? nextEnabled.groupMemento() : null; | ||||
| 
 | ||||
|       if (record.check()) { | ||||
|         count++; | ||||
|         count ++; | ||||
|         if (record.terminatesExpression()) { | ||||
|           updatedRecords = this._addRecord(updatedRecords, record); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (this._shouldNotifyDispatcher(currentRange, nextRange, currentGroup, nextGroup, updatedRecords)) { | ||||
|         currentRange.dispatcher.onRecordChange(currentGroup, updatedRecords); | ||||
|         updatedRecords = null; | ||||
|       } | ||||
| 
 | ||||
|       record = record.nextEnabled; | ||||
|     } | ||||
| 
 | ||||
|     return count; | ||||
|   } | ||||
| 
 | ||||
|   _groupChanged(currentRange, nextRange, currentGroup, nextGroup) { | ||||
|     return currentRange != nextRange || currentGroup != nextGroup; | ||||
|   } | ||||
| 
 | ||||
|   _shouldNotifyDispatcher(currentRange, nextRange, currentGroup, nextGroup, updatedRecords) { | ||||
|     return this._groupChanged(currentRange, nextRange, currentGroup, nextGroup) && isPresent(updatedRecords); | ||||
|   } | ||||
| 
 | ||||
|   _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]; | ||||
|  | ||||
| @ -34,15 +34,19 @@ export class ProtoRecord { | ||||
|   arity:int; | ||||
|   name:string; | ||||
|   dest:any; | ||||
|   groupMemento:any; | ||||
| 
 | ||||
|   next:ProtoRecord; | ||||
| 
 | ||||
|   recordInConstruction:Record; | ||||
| 
 | ||||
|   constructor(recordRange:ProtoRecordRange, | ||||
|               mode:int, | ||||
|               funcOrValue, | ||||
|               arity:int, | ||||
|               name:string, | ||||
|               dest) { | ||||
|               dest, | ||||
|               groupMemento) { | ||||
| 
 | ||||
|     this.recordRange = recordRange; | ||||
|     this._mode = mode; | ||||
| @ -50,6 +54,7 @@ export class ProtoRecord { | ||||
|     this.arity = arity; | ||||
|     this.name = name; | ||||
|     this.dest = dest; | ||||
|     this.groupMemento = groupMemento; | ||||
| 
 | ||||
|     this.next = null; | ||||
|     // The concrete Record instantiated from this ProtoRecord
 | ||||
| @ -190,21 +195,20 @@ export class Record { | ||||
| 
 | ||||
|   check():boolean { | ||||
|     if (this.isCollection) { | ||||
|       var changed = this._checkCollection(); | ||||
|       if (changed) { | ||||
|         this._notifyDispatcher(); | ||||
|         return true; | ||||
|       } | ||||
|       return false; | ||||
|       return this._checkCollection(); | ||||
|     } else { | ||||
|       this.previousValue = this.currentValue; | ||||
|       this.currentValue = this._calculateNewValue(); | ||||
|       if (isSame(this.previousValue, this.currentValue)) return false; | ||||
|       this._updateDestination(); | ||||
|       return true; | ||||
|       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() { | ||||
|     // todo(vicb): compute this info only once in ctor ? (add a bit in mode not to grow the mem req)
 | ||||
|     if (this.dest instanceof Record) { | ||||
| @ -213,15 +217,9 @@ export class Record { | ||||
|       } else { | ||||
|         this.dest.updateContext(this.currentValue); | ||||
|       } | ||||
|     } else { | ||||
|       this._notifyDispatcher(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _notifyDispatcher() { | ||||
|     this.recordRange.dispatcher.onRecordChange(this, this.protoRecord.dest); | ||||
|   } | ||||
| 
 | ||||
|   // return whether the content has changed
 | ||||
|   _checkCollection():boolean { | ||||
|     switch(this.type) { | ||||
| @ -307,9 +305,21 @@ export class Record { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   terminatesExpression():boolean { | ||||
|     return !(this.dest instanceof Record); | ||||
|   } | ||||
| 
 | ||||
|   get isMarkerRecord() { | ||||
|     return this.type == RECORD_TYPE_MARKER; | ||||
|   } | ||||
| 
 | ||||
|   expressionMemento() { | ||||
|     return this.protoRecord.dest; | ||||
|   } | ||||
| 
 | ||||
|   groupMemento() { | ||||
|     return isPresent(this.protoRecord) ? this.protoRecord.groupMemento : null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function isSame(a, b) { | ||||
|  | ||||
| @ -45,21 +45,24 @@ export class ProtoRecordRange { | ||||
|    * Parses [ast] into [ProtoRecord]s and adds them to [ProtoRecordRange]. | ||||
|    * | ||||
|    * @param ast The expression to watch | ||||
|    * @param memento an opaque object which will be passed to WatchGroupDispatcher on | ||||
|    * @param expressionMemento an opaque object which will be passed to WatchGroupDispatcher on | ||||
|    *        detecting a change. | ||||
|    * @param content Wether to watch collection content (true) or reference (false, default) | ||||
|    */ | ||||
|   addRecordsFromAST(ast:AST, | ||||
|         memento, | ||||
|         expressionMemento, | ||||
|         groupMemento, | ||||
|         content:boolean = false) | ||||
|   { | ||||
|     if (this.recordCreator === null) { | ||||
|       this.recordCreator = new ProtoRecordCreator(this); | ||||
|     } | ||||
|      | ||||
|     if (content) { | ||||
|       ast = new Collection(ast); | ||||
|     } | ||||
|     this.recordCreator.createRecordsFromAST(ast, memento); | ||||
|      | ||||
|     this.recordCreator.createRecordsFromAST(ast, expressionMemento, groupMemento); | ||||
|   } | ||||
| 
 | ||||
|   // TODO(rado): the type annotation should be dispatcher:WatchGroupDispatcher.
 | ||||
| @ -85,6 +88,8 @@ export class ProtoRecordRange { | ||||
|     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; | ||||
|     } | ||||
| @ -378,6 +383,8 @@ class ProtoRecordCreator { | ||||
|   protoRecordRange:ProtoRecordRange; | ||||
|   headRecord:ProtoRecord; | ||||
|   tailRecord:ProtoRecord; | ||||
|   groupMemento:any; | ||||
| 
 | ||||
|   constructor(protoRecordRange) { | ||||
|     this.protoRecordRange = protoRecordRange; | ||||
|     this.headRecord = null; | ||||
| @ -491,12 +498,13 @@ class ProtoRecordCreator { | ||||
| 
 | ||||
|   visitTemplateBindings(ast, dest) {this._unsupported();} | ||||
| 
 | ||||
|   createRecordsFromAST(ast:AST, memento){ | ||||
|     ast.visit(this, memento); | ||||
|   createRecordsFromAST(ast:AST, expressionMemento:any, groupMemento:any){ | ||||
|     this.groupMemento = groupMemento; | ||||
|     ast.visit(this, expressionMemento); | ||||
|   } | ||||
| 
 | ||||
|   construct(recordType, funcOrValue, arity, name, dest) { | ||||
|     return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, name, dest); | ||||
|     return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, name, dest, this.groupMemento); | ||||
|   } | ||||
| 
 | ||||
|   add(protoRecord:ProtoRecord) { | ||||
|  | ||||
| @ -26,7 +26,7 @@ export function main() { | ||||
|   function createChangeDetector(memo:string, exp:string, context = null, formatters = null, | ||||
|                                 content = false) { | ||||
|     var prr = new ProtoRecordRange(); | ||||
|     prr.addRecordsFromAST(ast(exp), memo, content); | ||||
|     prr.addRecordsFromAST(ast(exp), memo, memo, content); | ||||
| 
 | ||||
|     var dispatcher = new LoggingDispatcher(); | ||||
|     var rr = prr.instantiate(dispatcher, formatters); | ||||
| @ -97,21 +97,21 @@ export function main() { | ||||
|       it("should support literal array", () => { | ||||
|         var c = createChangeDetector('array', '[1,2]'); | ||||
|         c["changeDetector"].detectChanges(); | ||||
|         expect(c["dispatcher"].loggedValues).toEqual([[1,2]]); | ||||
|         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]]); | ||||
|         expect(c["dispatcher"].loggedValues).toEqual([[[1,2]]]); | ||||
|       }); | ||||
| 
 | ||||
|       it("should support literal maps", () => { | ||||
|         var c = createChangeDetector('map', '{z:1}'); | ||||
|         c["changeDetector"].detectChanges(); | ||||
|         expect(MapWrapper.get(c["dispatcher"].loggedValues[0], 'z')).toEqual(1); | ||||
|         expect(MapWrapper.get(c["dispatcher"].loggedValues[0][0], 'z')).toEqual(1); | ||||
| 
 | ||||
|         c = createChangeDetector('map', '{z:a}', new TestData(1)); | ||||
|         c["changeDetector"].detectChanges(); | ||||
|         expect(MapWrapper.get(c["dispatcher"].loggedValues[0], 'z')).toEqual(1); | ||||
|         expect(MapWrapper.get(c["dispatcher"].loggedValues[0][0], 'z')).toEqual(1); | ||||
|       }); | ||||
| 
 | ||||
|       it("should support binary operations", () => { | ||||
| @ -242,6 +242,7 @@ export function main() { | ||||
| 
 | ||||
|           context.a = [0]; | ||||
|           cd.detectChanges(); | ||||
| 
 | ||||
|           expect(dsp.log).toEqual(["a=" + | ||||
|             arrayChangesAsString({ | ||||
|               collection: ['0[null->0]'], | ||||
| @ -343,10 +344,69 @@ export function main() { | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       describe("onGroupChange", () => { | ||||
|         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 LoggingDispatcher(); | ||||
|           var rr = prr.instantiate(dispatcher, null); | ||||
| 
 | ||||
|           var cd = new ChangeDetector(rr); | ||||
|           cd.detectChanges(); | ||||
| 
 | ||||
|           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 LoggingDispatcher(); | ||||
|           var rr = new RecordRange(null, dispatcher); | ||||
|           rr.addRange(prr.instantiate(dispatcher, null)); | ||||
|           rr.addRange(prr.instantiate(dispatcher, null)); | ||||
| 
 | ||||
|           var cd = new ChangeDetector(rr); | ||||
|           cd.detectChanges(); | ||||
| 
 | ||||
|           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()"), "b", 2); | ||||
| 
 | ||||
|           var dispatcher = new LoggingDispatcher(); | ||||
|           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); | ||||
| 
 | ||||
|           var cd = new ChangeDetector(rr); | ||||
|           cd.detectChanges(); | ||||
| 
 | ||||
|           expect(dispatcher.loggedValues).toEqual(['InvokeA', ['a'], 'InvokeB', 'InvokeC', ['b', 'c']]); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| class TestRecord { | ||||
|   a; | ||||
|   b; | ||||
|   c; | ||||
| } | ||||
| 
 | ||||
| class Person { | ||||
|   name:string; | ||||
|   address:Address; | ||||
| @ -387,6 +447,7 @@ class TestData { | ||||
| class LoggingDispatcher extends WatchGroupDispatcher { | ||||
|   log:List; | ||||
|   loggedValues:List; | ||||
| 
 | ||||
|   constructor() { | ||||
|     this.log = null; | ||||
|     this.loggedValues = null; | ||||
| @ -397,10 +458,18 @@ class LoggingDispatcher extends WatchGroupDispatcher { | ||||
|     this.log = ListWrapper.create(); | ||||
|     this.loggedValues = ListWrapper.create(); | ||||
|   } | ||||
|    | ||||
|   logValue(value) { | ||||
|     ListWrapper.push(this.loggedValues, value); | ||||
|   } | ||||
| 
 | ||||
|   onRecordChange(record:Record, context) { | ||||
|     ListWrapper.push(this.loggedValues, record.currentValue); | ||||
|     ListWrapper.push(this.log, context + '=' + this._asString(record.currentValue)); | ||||
|   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); | ||||
|   } | ||||
| 
 | ||||
|   _asString(value) { | ||||
|  | ||||
| @ -46,7 +46,7 @@ export function main() { | ||||
|   } | ||||
| 
 | ||||
|   function createRecord(rr) { | ||||
|     return new Record(rr, new ProtoRecord(null, 0, null, null, null, null), null); | ||||
|     return new Record(rr, new ProtoRecord(null, 0, null, null, null, null, null), null); | ||||
|   } | ||||
| 
 | ||||
|   describe('record range', () => { | ||||
|  | ||||
							
								
								
									
										5
									
								
								modules/core/src/compiler/interfaces.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								modules/core/src/compiler/interfaces.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| export class OnChange { | ||||
|   onChange(changes) { | ||||
|     throw "not implemented"; | ||||
|   } | ||||
| } | ||||
| @ -1,5 +1,5 @@ | ||||
| import {DOM, Element, Node, Text, DocumentFragment, TemplateElement} from 'facade/dom'; | ||||
| import {ListWrapper, MapWrapper, List} from 'facade/collection'; | ||||
| import {ListWrapper, MapWrapper, StringMapWrapper, List} from 'facade/collection'; | ||||
| import {ProtoRecordRange, RecordRange, WatchGroupDispatcher} from 'change_detection/record_range'; | ||||
| import {Record} from 'change_detection/record'; | ||||
| import {AST} from 'change_detection/parser/ast'; | ||||
| @ -12,6 +12,7 @@ import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang'; | ||||
| import {Injector} from 'di/di'; | ||||
| import {NgElement} from 'core/dom/element'; | ||||
| import {ViewPort} from './viewport'; | ||||
| import {OnChange} from './interfaces'; | ||||
| 
 | ||||
| const NG_BINDING_CLASS = 'ng-binding'; | ||||
| 
 | ||||
| @ -47,22 +48,55 @@ export class View { | ||||
|     this.viewPorts = null; | ||||
|   } | ||||
| 
 | ||||
|   onRecordChange(record:Record, target) { | ||||
|   onRecordChange(groupMemento, records:List<Record>) { | ||||
|     this._invokeMementoForRecords(records); | ||||
|     if (groupMemento instanceof DirectivePropertyGroupMemento) { | ||||
|       this._notifyDirectiveAboutChanges(groupMemento, records); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _invokeMementoForRecords(records:List<Record>) { | ||||
|     for(var i = 0; i < records.length; ++i) { | ||||
|       this._invokeMementoFor(records[i]); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _notifyDirectiveAboutChanges(groupMemento, records:List<Record>) { | ||||
|     var dir = groupMemento.directive(this.elementInjectors); | ||||
|     if (dir instanceof OnChange) { | ||||
|       dir.onChange(this._collectChanges(records)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|     // dispatch to element injector or text nodes based on context
 | ||||
|     if (target instanceof DirectivePropertyMemento) { | ||||
|   _invokeMementoFor(record:Record) { | ||||
|     var memento = record.expressionMemento(); | ||||
|     if (memento instanceof DirectivePropertyMemento) { | ||||
|       // we know that it is DirectivePropertyMemento
 | ||||
|       var directiveMemento:DirectivePropertyMemento = target; | ||||
|       var directiveMemento:DirectivePropertyMemento = memento; | ||||
|       directiveMemento.invoke(record, this.elementInjectors); | ||||
|     } else if (target instanceof ElementPropertyMemento) { | ||||
|       var elementMemento:ElementPropertyMemento = target; | ||||
| 
 | ||||
|     } else if (memento instanceof ElementPropertyMemento) { | ||||
|       var elementMemento:ElementPropertyMemento = memento; | ||||
|       elementMemento.invoke(record, this.bindElements); | ||||
| 
 | ||||
|     } else { | ||||
|       // we know it refers to _textNodes.
 | ||||
|       var textNodeIndex:number = target; | ||||
|       var textNodeIndex:number = memento; | ||||
|       DOM.setText(this.textNodes[textNodeIndex], record.currentValue); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _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); | ||||
|     } | ||||
|     return changes; | ||||
|   } | ||||
| 
 | ||||
|   addViewPort(viewPort: ViewPort) { | ||||
|     if (isBlank(this.viewPorts)) this.viewPorts = []; | ||||
|     ListWrapper.push(this.viewPorts, viewPort); | ||||
| @ -163,7 +197,8 @@ export class ProtoView { | ||||
|       elBinder.textNodeIndices = ListWrapper.create(); | ||||
|     } | ||||
|     ListWrapper.push(elBinder.textNodeIndices, indexInParent); | ||||
|     this.protoRecordRange.addRecordsFromAST(expression, this.textNodesWithBindingCount++); | ||||
|     var memento = this.textNodesWithBindingCount++; | ||||
|     this.protoRecordRange.addRecordsFromAST(expression, memento, memento); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
| @ -175,12 +210,8 @@ export class ProtoView { | ||||
|       elBinder.hasElementPropertyBindings = true; | ||||
|       this.elementsWithBindingCount++; | ||||
|     } | ||||
|     this.protoRecordRange.addRecordsFromAST(expression, | ||||
|       new ElementPropertyMemento( | ||||
|         this.elementsWithBindingCount-1, | ||||
|         propertyName | ||||
|       ) | ||||
|     ); | ||||
|     var memento = new ElementPropertyMemento(this.elementsWithBindingCount-1, propertyName); | ||||
|     this.protoRecordRange.addRecordsFromAST(expression, memento, memento); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
| @ -202,15 +233,15 @@ export class ProtoView { | ||||
|     expression:AST, | ||||
|     setterName:string, | ||||
|     setter:SetterFn) { | ||||
|     this.protoRecordRange.addRecordsFromAST( | ||||
|       expression, | ||||
|       new DirectivePropertyMemento( | ||||
|         this.elementBinders.length-1, | ||||
|         directiveIndex, | ||||
|         setterName, | ||||
|         setter | ||||
|       ) | ||||
| 
 | ||||
|     var expMemento = new DirectivePropertyMemento( | ||||
|       this.elementBinders.length-1, | ||||
|       directiveIndex, | ||||
|       setterName, | ||||
|       setter | ||||
|     ); | ||||
|     var groupMemento = DirectivePropertyGroupMemento.get(expMemento); | ||||
|     this.protoRecordRange.addRecordsFromAST(expression, expMemento, groupMemento, false); | ||||
|   } | ||||
| 
 | ||||
|   static _createElementInjectors(elements, binders, hostElementInjector) { | ||||
| @ -363,6 +394,43 @@ export class DirectivePropertyMemento { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| var _groups = MapWrapper.create(); | ||||
| 
 | ||||
| class DirectivePropertyGroupMemento { | ||||
|   _elementInjectorIndex:number; | ||||
|   _directiveIndex:number; | ||||
| 
 | ||||
|   constructor(elementInjectorIndex:number, directiveIndex:number) { | ||||
|     this._elementInjectorIndex = elementInjectorIndex; | ||||
|     this._directiveIndex = directiveIndex; | ||||
|   } | ||||
| 
 | ||||
|   static get(memento:DirectivePropertyMemento) { | ||||
|     var elementInjectorIndex = memento._elementInjectorIndex; | ||||
|     var directiveIndex = memento._directiveIndex; | ||||
|     var id = elementInjectorIndex * 100 + directiveIndex; | ||||
| 
 | ||||
|     if (! MapWrapper.contains(_groups, id)) { | ||||
|       return MapWrapper.set(_groups, id, new DirectivePropertyGroupMemento(elementInjectorIndex, directiveIndex)); | ||||
|     } | ||||
|     return MapWrapper.get(_groups, id); | ||||
|   } | ||||
| 
 | ||||
|   directive(elementInjectors:List<ElementInjector>) { | ||||
|     var elementInjector:ElementInjector = elementInjectors[this._elementInjectorIndex]; | ||||
|     return elementInjector.getAtIndex(this._directiveIndex); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class PropertyUpdate { | ||||
|   currentValue; | ||||
|   previousValue; | ||||
| 
 | ||||
|   constructor(currentValue, previousValue) { | ||||
|     this.currentValue = currentValue; | ||||
|     this.previousValue = previousValue; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| //TODO(tbosch): I don't like to have done be called from a different place than notify
 | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
|  * Define public API for Angular here | ||||
|  */ | ||||
| export * from './annotations/annotations'; | ||||
| export * from './compiler/interfaces'; | ||||
| export * from './annotations/template_config'; | ||||
| 
 | ||||
| export * from './application'; | ||||
|  | ||||
| @ -3,6 +3,7 @@ import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/ | ||||
| import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector'; | ||||
| import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; | ||||
| import {Component, Decorator, Template} from 'core/annotations/annotations'; | ||||
| import {OnChange} from 'core/core'; | ||||
| import {ProtoRecordRange} from 'change_detection/record_range'; | ||||
| import {ChangeDetector} from 'change_detection/change_detector'; | ||||
| import {TemplateConfig} from 'core/annotations/template_config'; | ||||
| @ -281,7 +282,7 @@ export function main() { | ||||
|           expect(view.bindElements[0].id).toEqual('buz'); | ||||
|         }); | ||||
| 
 | ||||
|         it('should consume directive watch expression change.', () => { | ||||
|         it('should consume directive watch expression change', () => { | ||||
|           var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), | ||||
|             new ProtoRecordRange()); | ||||
|           pv.bindElement(new ProtoElementInjector(null, 0, [SomeDirective])); | ||||
| @ -292,6 +293,43 @@ export function main() { | ||||
|           cd.detectChanges(); | ||||
|           expect(view.elementInjectors[0].get(SomeDirective).prop).toEqual('buz'); | ||||
|         }); | ||||
| 
 | ||||
|         it('should notify a directive about changes after all its properties have been set', () => { | ||||
|           var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), | ||||
|             new ProtoRecordRange()); | ||||
| 
 | ||||
|           pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange])); | ||||
|           pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a')); | ||||
|           pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b')); | ||||
|           createView(pv); | ||||
| 
 | ||||
|           ctx.a = 100; | ||||
|           ctx.b = 200; | ||||
|           cd.detectChanges(); | ||||
| 
 | ||||
|           var directive = view.elementInjectors[0].get(DirectiveImplementingOnChange); | ||||
|           expect(directive.c).toEqual(300); | ||||
|         }); | ||||
| 
 | ||||
|         it('should provide a map of updated properties', () => { | ||||
|           var pv = new ProtoView(createElement('<div class="ng-binding"></div>'), | ||||
|             new ProtoRecordRange()); | ||||
| 
 | ||||
|           pv.bindElement(new ProtoElementInjector(null, 0, [DirectiveImplementingOnChange])); | ||||
|           pv.bindDirectiveProperty( 0, parser.parseBinding('a').ast, 'a', reflector.setter('a')); | ||||
|           pv.bindDirectiveProperty( 0, parser.parseBinding('b').ast, 'b', reflector.setter('b')); | ||||
|           createView(pv); | ||||
|           ctx.a = 0; | ||||
|           ctx.b = 0; | ||||
|           cd.detectChanges(); | ||||
| 
 | ||||
|           ctx.a = 100; | ||||
|           cd.detectChanges(); | ||||
| 
 | ||||
|           var directive = view.elementInjectors[0].get(DirectiveImplementingOnChange); | ||||
|           expect(directive.changes["a"].currentValue).toEqual(100); | ||||
|           expect(directive.changes["b"]).not.toBeDefined(); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @ -324,6 +362,18 @@ class SomeDirective { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class DirectiveImplementingOnChange extends OnChange { | ||||
|   a; | ||||
|   b; | ||||
|   c; | ||||
|   changes; | ||||
| 
 | ||||
|   onChange(changes) { | ||||
|     this.c = this.a + this.b; | ||||
|     this.changes = changes; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class SomeService {} | ||||
| 
 | ||||
| @Component({ | ||||
| @ -368,6 +418,8 @@ class AnotherDirective { | ||||
| 
 | ||||
| class MyEvaluationContext { | ||||
|   foo:string; | ||||
|   a; | ||||
|   b; | ||||
|   constructor() { | ||||
|     this.foo = 'bar'; | ||||
|   }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user