diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index bc57a08644..bbd565424b 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -39,10 +39,12 @@ export class ProtoWatchGroup { this.tailRecord = tail; } - instantiate(dispatcher:WatchGroupDispatcher):WatchGroup { + // TODO(rado): the type annotation should be dispatcher:WatchGroupDispatcher. + // but @Implements is not ready yet. + instantiate(dispatcher):WatchGroup { var watchGroup:WatchGroup = new WatchGroup(this, dispatcher); var tail:Record = null; - var proto:ProtoRecord; + var proto:ProtoRecord = null; var prevRecord:Record = null; if (this.headRecord !== null) { @@ -70,7 +72,9 @@ export class WatchGroup { @FIELD('final dispatcher:WatchGroupDispatcher') @FIELD('final headRecord:Record') @FIELD('final tailRecord:Record') - constructor(protoWatchGroup:ProtoWatchGroup, dispatcher:WatchGroupDispatcher) { + // TODO(rado): the type annotation should be dispatcher:WatchGroupDispatcher. + // but @Implements is not ready yet. + constructor(protoWatchGroup:ProtoWatchGroup, dispatcher) { this.protoWatchGroup = protoWatchGroup; this.dispatcher = dispatcher; this.headRecord = null; diff --git a/modules/core/src/compiler/view.js b/modules/core/src/compiler/view.js index beac7e0fe3..9ff34a8cae 100644 --- a/modules/core/src/compiler/view.js +++ b/modules/core/src/compiler/view.js @@ -6,6 +6,7 @@ import {ProtoElementInjector, ElementInjector} from './element_injector'; import {SetterFn} from 'change_detection/facade'; import {FIELD, IMPLEMENTS, int, isPresent, isBlank} from 'facade/lang'; import {List} from 'facade/collection'; +import {Injector} from 'di/di'; /*** * Const of making objects: http://jsperf.com/instantiate-size-of-object @@ -24,7 +25,8 @@ export class View { @FIELD('final nodes:List') @FIELD('final onChangeDispatcher:OnChangeDispatcher') constructor(fragment:DocumentFragment, elementInjector:List, - rootElementInjectors:List, textNodes:List, bindElements:List) { + rootElementInjectors:List, textNodes:List, bindElements:List, + protoWatchGroup:ProtoWatchGroup, context) { this.fragment = fragment; this.nodes = ListWrapper.clone(fragment.childNodes); this.elementInjectors = elementInjector; @@ -32,6 +34,8 @@ export class View { this.onChangeDispatcher = null; this.textNodes = textNodes; this.bindElements = bindElements; + this.watchGroup = protoWatchGroup.instantiate(this); + this.watchGroup.setContext(context); } onRecordChange(record:Record, target) { @@ -66,28 +70,29 @@ export class ProtoView { this._template = template; this._bindings = bindings; this._protoElementInjectors = protoElementInjectors; + this._protoWatchGroup = protoWatchGroup; // not implemented - this._protoWatchGroup = protoWatchGroup; this._useRootElement = useRootElement; } - instantiate():View { + instantiate(context, appInjector:Injector):View { var fragment = DOM.clone(this._template.content); var elements = DOM.querySelectorAll(fragment, ".ng-binding"); var protos = this._protoElementInjectors; /** * TODO: vsavkin: benchmark - * If this performs poorly, the three loops can be collapsed into one. + * If this performs poorly, the five loops can be collapsed into one. */ var elementInjectors = ProtoView._createElementInjectors(elements, protos); var rootElementInjectors = ProtoView._rootElementInjectors(elementInjectors); var textNodes = ProtoView._textNodes(elements, protos); var bindElements = ProtoView._bindElements(elements, protos); + ProtoView._instantiateDirectives(elementInjectors, appInjector); return new View(fragment, elementInjectors, rootElementInjectors, textNodes, - bindElements); + bindElements, this._protoWatchGroup, context); } static _createElementInjectors(elements, protos) { @@ -103,6 +108,13 @@ export class ProtoView { return injectors; } + static _instantiateDirectives( + injectors:List, appInjector:Injector) { + for (var i = 0; i < injectors.length; ++i) { + if (injectors[i] != null) injectors[i].instantiateDirectives(appInjector); + } + } + static _createElementInjector(element, proto) { //TODO: vsavkin: pass element to `proto.instantiate()` once https://github.com/angular/angular/pull/98 is merged return proto.hasBindings ? proto.instantiate({view:null}) : null; diff --git a/modules/core/test/compiler/view_spec.js b/modules/core/test/compiler/view_spec.js index 109a1cc849..d9b79f9d65 100644 --- a/modules/core/test/compiler/view_spec.js +++ b/modules/core/test/compiler/view_spec.js @@ -2,8 +2,12 @@ import {describe, xit, it, expect, beforeEach} from 'test_lib/test_lib'; import {ProtoView, ElementPropertyMemento, DirectivePropertyMemento} from 'core/compiler/view'; import {Record} from 'change_detection/record'; import {ProtoElementInjector, ElementInjector} from 'core/compiler/element_injector'; +import {ProtoWatchGroup} from 'change_detection/watch_group'; +import {ChangeDetector} from 'change_detection/change_detector'; import {DOM, Element} from 'facade/dom'; import {FIELD} from 'facade/lang'; +import {ImplicitReceiver, FieldRead} from 'change_detection/parser/ast'; +import {ClosureMap} from 'change_detection/parser/closure_map'; class Directive { @FIELD('prop') @@ -13,32 +17,36 @@ class Directive { } export function main() { + var oneFieldAst = (fieldName) => + new FieldRead(new ImplicitReceiver(), fieldName, + (new ClosureMap()).getter(fieldName)); + describe('view', function() { var tempalteWithThreeTypesOfBindings = '
' + 'Hello {}!' + '
' + - 'don\'t show me' + + 'don\'t show me' + '
' + '
'; + function templateElInj() { + var sectionPI = new ProtoElementInjector(null, [], [0], false); + var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false); + var spanPI = new ProtoElementInjector(divPI, [], [], true); + return [sectionPI, divPI, spanPI]; + } + describe('ProtoView', function() { it('should create view instance and locate basic parts', function() { var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings); var diBindings = []; - - var sectionPI = new ProtoElementInjector(null, [], [0], false); - var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false); - var spanPI = new ProtoElementInjector(divPI, [], [], true); - var protoElementInjectors = [sectionPI, divPI, spanPI]; - - var protoWatchGroup = null; var hasSingleRoot = false; - var pv = new ProtoView(template, diBindings, protoElementInjectors, - protoWatchGroup, hasSingleRoot); + var pv = new ProtoView(template, diBindings, templateElInj(), + new ProtoWatchGroup(), hasSingleRoot); - var view = pv.instantiate(); + var view = pv.instantiate(null, null); var section = DOM.firstChild(template.content); @@ -63,8 +71,9 @@ export function main() { var sectionPI = new ProtoElementInjector(null, [Directive], [], false); var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false); - var pv = new ProtoView(template, [], [sectionPI, divPI], null, false); - var view = pv.instantiate(); + var pv = new ProtoView(template, [], [sectionPI, divPI], + new ProtoWatchGroup(), false); + var view = pv.instantiate(null, null); expect(view.rootElementInjectors.length).toEqual(1); }); @@ -73,20 +82,9 @@ export function main() { var view; beforeEach(() => { var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings); - - var diBindings = []; - - var sectionPI = new ProtoElementInjector(null, [], [0], false); - var divPI = new ProtoElementInjector(sectionPI, [Directive], [], false); - var spanPI = new ProtoElementInjector(divPI, [], [], true); - var protoElementInjectors = [sectionPI, divPI, spanPI]; - - var protoWatchGroup = null; - var hasSingleRoot = false; - var pv = new ProtoView(template, diBindings, protoElementInjectors, - protoWatchGroup, hasSingleRoot); - - view = pv.instantiate(); + var pv = new ProtoView(template, [], templateElInj(), + new ProtoWatchGroup(), false); + view = pv.instantiate(null, null); }); it('should consume text node changes', () => { @@ -98,20 +96,17 @@ export function main() { it('should consume element binding changes', () => { var elementWithBinding = view.bindElements[0]; - expect(elementWithBinding.hidden).toEqual(false); + expect(elementWithBinding.id).toEqual(''); var record = new Record(null, null); - var memento = new ElementPropertyMemento(0, 'hidden'); - record.currentValue = true; + var memento = new ElementPropertyMemento(0, 'id'); + record.currentValue = 'foo'; view.onRecordChange(record, memento); - expect(elementWithBinding.hidden).toEqual(true); + expect(elementWithBinding.id).toEqual('foo'); }); it('should consume directive watch expression change.', () => { var elInj = view.elementInjectors[1]; - // TODO(rado): hook-up instantiateDirectives in implementation and - // remove from here. - elInj.instantiateDirectives(null); expect(elInj.get(Directive).prop).toEqual('foo'); var record = new Record(null, null); var memento = new DirectivePropertyMemento(1, 0, 'prop', @@ -121,6 +116,64 @@ export function main() { expect(elInj.get(Directive).prop).toEqual('bar'); }); }); + + describe('integration view update with change detector', () => { + var view, cd, ctx; + function setUp(memento) { + var template = DOM.createTemplate(tempalteWithThreeTypesOfBindings); + + var protoWatchGroup = new ProtoWatchGroup(); + protoWatchGroup.watch(oneFieldAst('foo'), memento); + + var pv = new ProtoView(template, [], templateElInj(), + protoWatchGroup, false); + + ctx = new MyEvaluationContext(); + view = pv.instantiate(ctx, null); + + cd = new ChangeDetector(view.watchGroup); + } + + it('should consume text node changes', () => { + setUp(0); + + ctx.foo = 'buz'; + cd.detectChanges(); + expect(view.textNodes[0].nodeValue).toEqual('buz'); + }); + + it('should consume element binding changes', () => { + setUp(new ElementPropertyMemento(0, 'id')); + + var elementWithBinding = view.bindElements[0]; + expect(elementWithBinding.id).toEqual(''); + + ctx.foo = 'buz'; + cd.detectChanges(); + expect(elementWithBinding.id).toEqual('buz'); + }); + + it('should consume directive watch expression change.', () => { + var memento = new DirectivePropertyMemento(1, 0, 'prop', + (o, v) => o.prop = v); + setUp(memento); + + var elInj = view.elementInjectors[1]; + expect(elInj.get(Directive).prop).toEqual('foo'); + + ctx.foo = 'buz'; + cd.detectChanges(); + expect(elInj.get(Directive).prop).toEqual('buz'); + }); + + }); }); }); } + +class MyEvaluationContext { + @FIELD('foo') + constructor() { + this.foo = 'bar'; + }; +}