diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index 1bb72ba82c..7c72951e6f 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -383,7 +383,7 @@ export class ElementInjector extends TreeNode implements Depend this._preBuiltObjects = null; this._strategy.callOnDestroy(); this._strategy.dehydrate(); - this._queryStrategy.clearQueryLists(); + this._queryStrategy.dehydrate(); } hydrate(imperativelyCreatedInjector: Injector, host: ElementInjector, @@ -392,6 +392,7 @@ export class ElementInjector extends TreeNode implements Depend this._preBuiltObjects = preBuiltObjects; this._reattachInjectors(imperativelyCreatedInjector); + this._queryStrategy.hydrate(); this._strategy.hydrate(); this.hydrated = true; @@ -604,7 +605,8 @@ export class ElementInjector extends TreeNode implements Depend interface _QueryStrategy { setContentQueriesAsDirty(): void; setViewQueriesAsDirty(): void; - clearQueryLists(): void; + hydrate(): void; + dehydrate(): void; updateContentQueries(): void; updateViewQueries(): void; findQuery(query: QueryMetadata): QueryRef; @@ -613,7 +615,8 @@ interface _QueryStrategy { class _EmptyQueryStrategy implements _QueryStrategy { setContentQueriesAsDirty(): void {} setViewQueriesAsDirty(): void {} - clearQueryLists(): void {} + hydrate(): void {} + dehydrate(): void {} updateContentQueries(): void {} updateViewQueries(): void {} findQuery(query: QueryMetadata): QueryRef { @@ -632,9 +635,9 @@ class InlineQueryStrategy implements _QueryStrategy { constructor(ei: ElementInjector) { var protoRefs = ei._proto.protoQueryRefs; - if (protoRefs.length > 0) this.query0 = new QueryRef(protoRefs[0], new QueryList(), ei); - if (protoRefs.length > 1) this.query1 = new QueryRef(protoRefs[1], new QueryList(), ei); - if (protoRefs.length > 2) this.query2 = new QueryRef(protoRefs[2], new QueryList(), ei); + if (protoRefs.length > 0) this.query0 = new QueryRef(protoRefs[0], ei); + if (protoRefs.length > 1) this.query1 = new QueryRef(protoRefs[1], ei); + if (protoRefs.length > 2) this.query2 = new QueryRef(protoRefs[2], ei); } setContentQueriesAsDirty(): void { @@ -649,39 +652,39 @@ class InlineQueryStrategy implements _QueryStrategy { if (isPresent(this.query2) && this.query2.isViewQuery) this.query2.dirty = true; } - clearQueryLists(): void { - if (isPresent(this.query0)) this.query0.reset(); - if (isPresent(this.query1)) this.query1.reset(); - if (isPresent(this.query2)) this.query2.reset(); + hydrate(): void { + if (isPresent(this.query0)) this.query0.hydrate(); + if (isPresent(this.query1)) this.query1.hydrate(); + if (isPresent(this.query2)) this.query2.hydrate(); + } + + dehydrate(): void { + if (isPresent(this.query0)) this.query0.dehydrate(); + if (isPresent(this.query1)) this.query1.dehydrate(); + if (isPresent(this.query2)) this.query2.dehydrate(); } updateContentQueries() { if (isPresent(this.query0) && !this.query0.isViewQuery) { this.query0.update(); - this.query0.list.fireCallbacks(); } if (isPresent(this.query1) && !this.query1.isViewQuery) { this.query1.update(); - this.query1.list.fireCallbacks(); } if (isPresent(this.query2) && !this.query2.isViewQuery) { this.query2.update(); - this.query2.list.fireCallbacks(); } } updateViewQueries() { if (isPresent(this.query0) && this.query0.isViewQuery) { this.query0.update(); - this.query0.list.fireCallbacks(); } if (isPresent(this.query1) && this.query1.isViewQuery) { this.query1.update(); - this.query1.list.fireCallbacks(); } if (isPresent(this.query2) && this.query2.isViewQuery) { this.query2.update(); - this.query2.list.fireCallbacks(); } } @@ -703,7 +706,7 @@ class DynamicQueryStrategy implements _QueryStrategy { queries: QueryRef[]; constructor(ei: ElementInjector) { - this.queries = ei._proto.protoQueryRefs.map(p => new QueryRef(p, new QueryList(), ei)); + this.queries = ei._proto.protoQueryRefs.map(p => new QueryRef(p, ei)); } setContentQueriesAsDirty(): void { @@ -720,10 +723,17 @@ class DynamicQueryStrategy implements _QueryStrategy { } } - clearQueryLists(): void { + hydrate(): void { for (var i = 0; i < this.queries.length; ++i) { var q = this.queries[i]; - q.reset(); + q.hydrate(); + } + } + + dehydrate(): void { + for (var i = 0; i < this.queries.length; ++i) { + var q = this.queries[i]; + q.dehydrate(); } } @@ -732,7 +742,6 @@ class DynamicQueryStrategy implements _QueryStrategy { var q = this.queries[i]; if (!q.isViewQuery) { q.update(); - q.list.fireCallbacks(); } } } @@ -742,7 +751,6 @@ class DynamicQueryStrategy implements _QueryStrategy { var q = this.queries[i]; if (q.isViewQuery) { q.update(); - q.list.fireCallbacks(); } } } @@ -972,8 +980,10 @@ export class ProtoQueryRef { } export class QueryRef { - constructor(public protoQueryRef: ProtoQueryRef, public list: QueryList, - private originator: ElementInjector, public dirty: boolean = true) {} + public list: QueryList; + public dirty: boolean; + + constructor(public protoQueryRef: ProtoQueryRef, private originator: ElementInjector) {} get isViewQuery(): boolean { return this.protoQueryRef.query.isViewQuery; } @@ -991,6 +1001,8 @@ export class QueryRef { this.protoQueryRef.setter(dir, this.list); } } + + this.list.notifyOnChanges(); } private _update(): void { @@ -1073,9 +1085,10 @@ export class QueryRef { inj.addDirectivesMatchingQuery(this.protoQueryRef.query, aggregator); } - reset(): void { - this.list.reset([]); - this.list.removeAllCallbacks(); + dehydrate(): void { this.list = null; } + + hydrate(): void { + this.list = new QueryList(); this.dirty = true; } } diff --git a/modules/angular2/src/core/compiler/query_list.dart b/modules/angular2/src/core/compiler/query_list.dart index 64f4d6ec01..a101396f50 100644 --- a/modules/angular2/src/core/compiler/query_list.dart +++ b/modules/angular2/src/core/compiler/query_list.dart @@ -1,6 +1,7 @@ library angular2.src.core.compiler.query_list; import 'dart:collection'; +import 'package:angular2/src/core/facade/async.dart'; /** * See query_list.ts @@ -8,33 +9,11 @@ import 'dart:collection'; class QueryList extends Object with IterableMixin { List _results = []; - List _callbacks = []; - bool _dirty = false; + EventEmitter _emitter = new EventEmitter(); Iterator get iterator => _results.iterator; - /** @private */ - void reset(List newList) { - _results = newList; - _dirty = true; - } - - void add(T obj) { - _results.add(obj); - _dirty = true; - } - - void onChange(callback) { - _callbacks.add(callback); - } - - void removeCallback(callback) { - _callbacks.remove(callback); - } - - void removeAllCallbacks() { - this._callbacks = []; - } + Stream> get changes => _emitter; int get length => _results.length; T get first => _results.first; @@ -49,10 +28,12 @@ class QueryList extends Object } /** @private */ - void fireCallbacks() { - if (_dirty) { - _callbacks.forEach((c) => c()); - _dirty = false; - } + void reset(List newList) { + _results = newList; + } + + /** @private */ + void notifyOnChanges() { + _emitter.add(this); } } diff --git a/modules/angular2/src/core/compiler/query_list.ts b/modules/angular2/src/core/compiler/query_list.ts index 371669295f..7d2c77cb6c 100644 --- a/modules/angular2/src/core/compiler/query_list.ts +++ b/modules/angular2/src/core/compiler/query_list.ts @@ -1,5 +1,6 @@ import {ListWrapper, MapWrapper} from 'angular2/src/core/facade/collection'; import {getSymbolIterator} from 'angular2/src/core/facade/lang'; +import {Observable, EventEmitter} from 'angular2/src/core/facade/async'; /** @@ -12,7 +13,7 @@ import {getSymbolIterator} from 'angular2/src/core/facade/lang'; * javascript `for (var i of items)` loops as well as in Angular templates with * `*ng-for="#i of myList"`. * - * Changes can be observed by attaching callbacks. + * Changes can be observed by subscribing to the changes `Observable`. * * NOTE: In the future this class will implement an `Observable` interface. * @@ -21,45 +22,16 @@ import {getSymbolIterator} from 'angular2/src/core/facade/lang'; * @Component({...}) * class Container { * constructor(@Query(Item) items: QueryList) { - * items.onChange(() => console.log(items.length)); + * items.changes.subscribe(_ => console.log(items.length)); * } * } * ``` */ export class QueryList { - protected _results: Array < T >= []; - protected _callbacks: Array < () => void >= []; - protected _dirty: boolean = false; - - /** @private */ - reset(newList: T[]): void { - this._results = newList; - this._dirty = true; - } - - /** @private */ - add(obj: T): void { - this._results.push(obj); - this._dirty = true; - } - - /** - * registers a callback that is called upon each change. - */ - onChange(callback: () => void): void { this._callbacks.push(callback); } - - /** - * removes a given callback. - */ - removeCallback(callback: () => void): void { ListWrapper.remove(this._callbacks, callback); } - - /** - * removes all callback that have been attached. - */ - removeAllCallbacks(): void { this._callbacks = []; } - - toString(): string { return this._results.toString(); } + private _results: Array = []; + private _emitter = new EventEmitter(); + get changes(): Observable { return this._emitter; } get length(): number { return this._results.length; } get first(): T { return ListWrapper.first(this._results); } get last(): T { return ListWrapper.last(this._results); } @@ -71,11 +43,13 @@ export class QueryList { [getSymbolIterator()](): any { return this._results[getSymbolIterator()](); } + toString(): string { return this._results.toString(); } + + /** + * @private + */ + reset(res: T[]): void { this._results = res; } + /** @private */ - fireCallbacks(): void { - if (this._dirty) { - ListWrapper.forEach(this._callbacks, (c) => c()); - this._dirty = false; - } - } + notifyOnChanges(): void { this._emitter.next(this); } } diff --git a/modules/angular2/src/core/forms/directives/select_control_value_accessor.ts b/modules/angular2/src/core/forms/directives/select_control_value_accessor.ts index 81339616c0..69b528240d 100644 --- a/modules/angular2/src/core/forms/directives/select_control_value_accessor.ts +++ b/modules/angular2/src/core/forms/directives/select_control_value_accessor.ts @@ -6,6 +6,7 @@ import {Query, Directive} from 'angular2/src/core/metadata'; import {NgControl} from './ng_control'; import {ControlValueAccessor} from './control_value_accessor'; import {isPresent} from 'angular2/src/core/facade/lang'; +import {ObservableWrapper} from 'angular2/src/core/facade/async'; import {setProperty} from './shared'; /** @@ -81,6 +82,6 @@ export class SelectControlValueAccessor implements ControlValueAccessor { registerOnTouched(fn: () => any): void { this.onTouched = fn; } private _updateValueWhenListOfOptionsChanges(query: QueryList) { - query.onChange(() => this.writeValue(this.value)); + ObservableWrapper.subscribe(query.changes, (_) => this.writeValue(this.value)); } } diff --git a/modules/angular2/test/core/compiler/query_integration_spec.ts b/modules/angular2/test/core/compiler/query_integration_spec.ts index 00c9c1ca5d..128f689a39 100644 --- a/modules/angular2/test/core/compiler/query_integration_spec.ts +++ b/modules/angular2/test/core/compiler/query_integration_spec.ts @@ -13,6 +13,7 @@ import { } from 'angular2/test_lib'; import {isPresent} from 'angular2/src/core/facade/lang'; +import {ObservableWrapper} from 'angular2/src/core/facade/async'; import { Component, @@ -263,7 +264,7 @@ export function main() { }); - describe("onChange", () => { + describe("changes", () => { it('should notify query on change', inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { var template = '' + @@ -277,7 +278,7 @@ export function main() { var q = view.debugElement.componentViewChildren[0].getLocal("q"); view.detectChanges(); - q.query.onChange(() => { + ObservableWrapper.subscribe(q.query.changes, (_) => { expect(q.query.first.text).toEqual("1"); expect(q.query.last.text).toEqual("2"); async.done(); @@ -304,8 +305,8 @@ export function main() { var firedQ2 = false; - q2.query.onChange(() => { firedQ2 = true; }); - q1.query.onChange(() => { + ObservableWrapper.subscribe(q2.query.changes, (_) => { firedQ2 = true; }); + ObservableWrapper.subscribe(q1.query.changes, (_) => { expect(firedQ2).toBe(true); async.done(); }); diff --git a/modules/angular2/test/core/compiler/query_list_spec.ts b/modules/angular2/test/core/compiler/query_list_spec.ts index 8b233600b3..63c6abe4d9 100644 --- a/modules/angular2/test/core/compiler/query_list_spec.ts +++ b/modules/angular2/test/core/compiler/query_list_spec.ts @@ -1,8 +1,21 @@ -import {describe, it, expect, beforeEach, ddescribe, iit, xit, el} from 'angular2/test_lib'; +import { + describe, + it, + expect, + beforeEach, + ddescribe, + iit, + xit, + el, + fakeAsync, + tick +} from 'angular2/test_lib'; import {MapWrapper, ListWrapper, iterateListLike} from 'angular2/src/core/facade/collection'; import {StringWrapper} from 'angular2/src/core/facade/lang'; +import {ObservableWrapper} from 'angular2/src/core/facade/async'; import {QueryList} from 'angular2/src/core/compiler/query_list'; +import {DOM} from 'angular2/src/core/dom/dom_adapter'; export function main() { @@ -16,95 +29,64 @@ export function main() { function logAppend(item) { log += (log.length == 0 ? '' : ', ') + item; } - it('should support adding objects and iterating over them', () => { - queryList.add('one'); - queryList.add('two'); - iterateListLike(queryList, logAppend); - expect(log).toEqual('one, two'); - }); - it('should support resetting and iterating over the new objects', () => { - queryList.add('one'); - queryList.add('two'); - queryList.reset(['one again']); - queryList.add('two again'); + queryList.reset(['one']); + queryList.reset(['two']); iterateListLike(queryList, logAppend); - expect(log).toEqual('one again, two again'); + expect(log).toEqual('two'); }); it('should support length', () => { - queryList.add('one'); - queryList.add('two'); + queryList.reset(['one', 'two']); expect(queryList.length).toEqual(2); }); it('should support map', () => { - queryList.add('one'); - queryList.add('two'); + queryList.reset(['one', 'two']); expect(queryList.map((x) => x)).toEqual(['one', 'two']); }); it('should support toString', () => { - queryList.add('one'); - queryList.add('two'); + queryList.reset(['one', 'two']); var listString = queryList.toString(); expect(StringWrapper.contains(listString, 'one')).toBeTruthy(); expect(StringWrapper.contains(listString, 'two')).toBeTruthy(); }); it('should support first and last', () => { - queryList.add('one'); - queryList.add('two'); - queryList.add('three'); + queryList.reset(['one', 'two', 'three']); expect(queryList.first).toEqual('one'); expect(queryList.last).toEqual('three'); }); - describe('simple observable interface', () => { - it('should fire callbacks on change', () => { - var fires = 0; - queryList.onChange(() => { fires += 1; }); + if (DOM.supportsDOMEvents()) { + describe('simple observable interface', () => { + it('should fire callbacks on change', fakeAsync(() => { + var fires = 0; + ObservableWrapper.subscribe(queryList.changes, (_) => { fires += 1; }); - queryList.fireCallbacks(); - expect(fires).toEqual(0); + queryList.notifyOnChanges(); + tick(); - queryList.add('one'); + expect(fires).toEqual(1); - queryList.fireCallbacks(); - expect(fires).toEqual(1); + queryList.notifyOnChanges(); + tick(); - queryList.fireCallbacks(); - expect(fires).toEqual(1); + expect(fires).toEqual(2); + })); + + it('should provides query list as an argument', fakeAsync(() => { + var recorded; + ObservableWrapper.subscribe(queryList.changes, (v: any) => { recorded = v; }); + + queryList.reset(["one"]); + queryList.notifyOnChanges(); + tick(); + + expect(recorded).toBe(queryList); + })); }); - - it('should support removing callbacks', () => { - var fires = 0; - var callback = () => fires += 1; - queryList.onChange(callback); - - queryList.add('one'); - queryList.fireCallbacks(); - expect(fires).toEqual(1); - - queryList.removeCallback(callback); - - queryList.add('two'); - queryList.fireCallbacks(); - expect(fires).toEqual(1); - }); - - it('should support removing all callbacks', () => { - var fires = 0; - var callback = () => fires += 1; - queryList.onChange(callback); - - queryList.add('one'); - queryList.removeAllCallbacks(); - - queryList.fireCallbacks(); - - expect(fires).toEqual(0); - }); - }); + } }); } diff --git a/modules/angular2/test/core/forms/integration_spec.ts b/modules/angular2/test/core/forms/integration_spec.ts index a7d1e92e8b..b9dd5eccbd 100644 --- a/modules/angular2/test/core/forms/integration_spec.ts +++ b/modules/angular2/test/core/forms/integration_spec.ts @@ -301,24 +301,26 @@ export function main() { })); it("should support `; - tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((rootTC) => { - rootTC.debugElement.componentInstance.form = - new ControlGroup({"city": new Control("NYC")}); - rootTC.debugElement.componentInstance.data = ['SF', 'NYC']; - rootTC.detectChanges(); + var rootTC; + tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((rtc) => rootTC = rtc); + tick(); - var select = rootTC.debugElement.query(By.css('select')); - expect(select.nativeElement.value).toEqual('NYC'); - async.done(); - }); - })); + rootTC.debugElement.componentInstance.form = + new ControlGroup({"city": new Control("NYC")}); + rootTC.debugElement.componentInstance.data = ['SF', 'NYC']; + rootTC.detectChanges(); + tick(); + + var select = rootTC.debugElement.query(By.css('select')); + expect(select.nativeElement.value).toEqual('NYC'); + }))); it("should support custom value accessors", inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 8a1a61d71d..bcdc807327 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -809,13 +809,12 @@ var NG_API = [ 'Query.token', 'Query.varBindings', 'QueryList', - 'QueryList.add()', 'QueryList.any():dart', 'QueryList.contains():dart', 'QueryList.elementAt():dart', 'QueryList.every():dart', 'QueryList.expand():dart', - 'QueryList.fireCallbacks():', + 'QueryList.notifyOnChanges():', 'QueryList.first', 'QueryList.firstWhere():dart', 'QueryList.fold():dart', @@ -828,10 +827,8 @@ var NG_API = [ 'QueryList.lastWhere():dart', 'QueryList.length', 'QueryList.map()', - 'QueryList.onChange()', + 'QueryList.changes', 'QueryList.reduce():dart', - 'QueryList.removeAllCallbacks()', - 'QueryList.removeCallback()', 'QueryList.reset()', 'QueryList.single', 'QueryList.singleWhere():dart',