feat(query): notify on changes

This commit is contained in:
vsavkin 2015-06-12 09:45:31 -07:00
parent 73d152506b
commit 5bfcca2d5b
11 changed files with 129 additions and 7 deletions

View File

@ -203,7 +203,12 @@ export class ChangeDetectorJITGenerator {
} }
} }
return notifications.join("\n"); var directiveNotifications = notifications.join("\n");
return `
this.dispatcher.notifyOnAllChangesDone();
${directiveNotifications}
`;
} }
_genLocalDefinitions(): string { return this._localNames.map((n) => `var ${n};`).join("\n"); } _genLocalDefinitions(): string { return this._localNames.map((n) => `var ${n};`).join("\n"); }

View File

@ -110,6 +110,7 @@ export class DynamicChangeDetector extends AbstractChangeDetector {
} }
callOnAllChangesDone() { callOnAllChangesDone() {
this.dispatcher.notifyOnAllChangesDone();
var dirs = this.directiveRecords; var dirs = this.directiveRecords;
for (var i = dirs.length - 1; i >= 0; --i) { for (var i = dirs.length - 1; i >= 0; --i) {
var dir = dirs[i]; var dir = dirs[i];

View File

@ -26,7 +26,6 @@ export class BaseQueryList<T> {
this._dirty = true; this._dirty = true;
} }
// TODO(rado): hook up with change detection after #995.
fireCallbacks() { fireCallbacks() {
if (this._dirty) { if (this._dirty) {
ListWrapper.forEach(this._callbacks, (c) => c()); ListWrapper.forEach(this._callbacks, (c) => c());

View File

@ -707,6 +707,15 @@ export class ElementInjector extends TreeNode<ElementInjector> {
} }
} }
onAllChangesDone(): void {
if (isPresent(this._query0) && this._query0.originator === this)
this._query0.list.fireCallbacks();
if (isPresent(this._query1) && this._query1.originator === this)
this._query1.list.fireCallbacks();
if (isPresent(this._query2) && this._query2.originator === this)
this._query2.list.fireCallbacks();
}
hydrate(injector: Injector, host: ElementInjector, preBuiltObjects: PreBuiltObjects): void { hydrate(injector: Injector, host: ElementInjector, preBuiltObjects: PreBuiltObjects): void {
var p = this._proto; var p = this._proto;

View File

@ -111,6 +111,13 @@ export class AppView implements ChangeDispatcher, EventDispatcher {
} }
} }
notifyOnAllChangesDone(): void {
var ei = this.elementInjectors;
for (var i = ei.length - 1; i >= 0; i--) {
if (isPresent(ei[i])) ei[i].onAllChangesDone();
}
}
getDirectiveFor(directive: DirectiveIndex) { getDirectiveFor(directive: DirectiveIndex) {
var elementInjector = this.elementInjectors[directive.elementIndex]; var elementInjector = this.elementInjectors[directive.elementIndex];
return elementInjector.getDirectiveAtIndex(directive.directiveIndex); return elementInjector.getDirectiveAtIndex(directive.directiveIndex);

View File

@ -262,11 +262,15 @@ class _CodegenState {
/// them. /// them.
String _getCallOnAllChangesDoneBody() { String _getCallOnAllChangesDoneBody() {
// NOTE(kegluneq): Order is important! // NOTE(kegluneq): Order is important!
return _directiveRecords.reversed var directiveNotifications = _directiveRecords.reversed
.where((rec) => rec.callOnAllChangesDone) .where((rec) => rec.callOnAllChangesDone)
.map((rec) => .map((rec) =>
'${_genGetDirective(rec.directiveIndex)}.onAllChangesDone();') '${_genGetDirective(rec.directiveIndex)}.onAllChangesDone();')
.join(''); .join('');
return '''
_dispatcher.notifyOnAllChangesDone();
${directiveNotifications}
''';
} }
String _genLocalDefinitions() => String _genLocalDefinitions() =>

View File

@ -324,6 +324,12 @@ export function main() {
}); });
}); });
it('should notify the dispatcher on all changes done', () => {
var val = _createChangeDetector('name', new Person('bob'));
val.changeDetector.detectChanges();
expect(val.dispatcher.onAllChangesDoneCalled).toEqual(true);
});
describe('updating directives', () => { describe('updating directives', () => {
var directive1; var directive1;
var directive2; var directive2;
@ -975,6 +981,7 @@ class FakeDirectives {
class TestDispatcher extends ChangeDispatcher { class TestDispatcher extends ChangeDispatcher {
log: List<string>; log: List<string>;
loggedValues: List<any>; loggedValues: List<any>;
onAllChangesDoneCalled: boolean = false;
constructor() { constructor() {
super(); super();
@ -984,6 +991,7 @@ class TestDispatcher extends ChangeDispatcher {
clear() { clear() {
this.log = ListWrapper.create(); this.log = ListWrapper.create();
this.loggedValues = ListWrapper.create(); this.loggedValues = ListWrapper.create();
this.onAllChangesDoneCalled = true;
} }
notifyOnBinding(binding, value) { notifyOnBinding(binding, value) {
@ -991,6 +999,8 @@ class TestDispatcher extends ChangeDispatcher {
ListWrapper.push(this.loggedValues, value); ListWrapper.push(this.loggedValues, value);
} }
notifyOnAllChangesDone() { this.onAllChangesDoneCalled = true; }
_asString(value) { return (isBlank(value) ? 'null' : value.toString()); } _asString(value) { return (isBlank(value) ? 'null' : value.toString()); }
} }

View File

@ -12,6 +12,8 @@ import {
beforeEach, beforeEach,
SpyObject, SpyObject,
proxy, proxy,
inject,
AsyncTestCompleter,
el, el,
containsRegexp containsRegexp
} from 'angular2/test_lib'; } from 'angular2/test_lib';
@ -730,7 +732,7 @@ export function main() {
expect(d.dependency).toBeAnInstanceOf(SimpleDirective); expect(d.dependency).toBeAnInstanceOf(SimpleDirective);
}); });
it("should throw when a depenency cannot be resolved", () => { it("should throw when a dependency cannot be resolved", () => {
expect(() => injector(ListWrapper.concat([NeedsDirectiveFromParent], extraBindings))) expect(() => injector(ListWrapper.concat([NeedsDirectiveFromParent], extraBindings)))
.toThrowError(containsRegexp( .toThrowError(containsRegexp(
`No provider for ${stringify(SimpleDirective) }! (${stringify(NeedsDirectiveFromParent) } -> ${stringify(SimpleDirective) })`)); `No provider for ${stringify(SimpleDirective) }! (${stringify(NeedsDirectiveFromParent) } -> ${stringify(SimpleDirective) })`));
@ -818,7 +820,7 @@ export function main() {
}); });
describe("lifecycle", () => { describe("lifecycle", () => {
it("should call onDestroy on directives subscribed to this event", function() { it("should call onDestroy on directives subscribed to this event", () => {
var inj = injector(ListWrapper.concat( var inj = injector(ListWrapper.concat(
[DirectiveBinding.createFromType(DirectiveWithDestroy, [DirectiveBinding.createFromType(DirectiveWithDestroy,
new dirAnn.Directive({lifecycle: [onDestroy]}))], new dirAnn.Directive({lifecycle: [onDestroy]}))],
@ -828,13 +830,45 @@ export function main() {
expect(destroy.onDestroyCounter).toBe(1); expect(destroy.onDestroyCounter).toBe(1);
}); });
it("should work with services", function() { it("should work with services", () => {
var inj = injector(ListWrapper.concat( var inj = injector(ListWrapper.concat(
[DirectiveBinding.createFromType( [DirectiveBinding.createFromType(
SimpleDirective, new dirAnn.Directive({hostInjector: [SimpleService]}))], SimpleDirective, new dirAnn.Directive({hostInjector: [SimpleService]}))],
extraBindings)); extraBindings));
inj.dehydrate(); inj.dehydrate();
}); });
it("should notify queries", inject([AsyncTestCompleter], (async) => {
var inj = injector(ListWrapper.concat([NeedsQuery], extraBindings));
var query = inj.get(NeedsQuery).query;
query.add(new CountingDirective()); // this marks the query as dirty
query.onChange(() => async.done());
inj.onAllChangesDone();
}));
it("should not notify inherited queries", inject([AsyncTestCompleter], (async) => {
var child = parentChildInjectors(ListWrapper.concat([NeedsQuery], extraBindings), []);
var query = child.parent.get(NeedsQuery).query;
var calledOnChange = false;
query.onChange(() => {
// make sure the callback is called only once
expect(calledOnChange).toEqual(false);
expect(query.length).toEqual(2);
calledOnChange = true;
async.done()
});
query.add(new CountingDirective());
child.onAllChangesDone(); // this does not notify the query
query.add(new CountingDirective());
child.parent.onAllChangesDone();
}));
}); });
describe("dynamicallyCreateComponent", () => { describe("dynamicallyCreateComponent", () => {

View File

@ -18,6 +18,7 @@ import {QueryList} from 'angular2/core';
import {Query, Component, Directive, View} from 'angular2/annotations'; import {Query, Component, Directive, View} from 'angular2/annotations';
import {NgIf, NgFor} from 'angular2/angular2'; import {NgIf, NgFor} from 'angular2/angular2';
import {ListWrapper} from 'angular2/src/facade/collection';
import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter'; import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter';
@ -120,6 +121,55 @@ export function main() {
async.done(); async.done();
}); });
})); }));
it('should notify query on change',
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
var template = '<needs-query-desc #q>' +
'<div text="1"></div>' +
'<div *ng-if="shouldShow" text="2"></div>' +
'</needs-query-desc>';
tb.createView(MyComp, {html: template})
.then((view) => {
var q = view.rawView.locals.get("q");
view.detectChanges();
q.query.onChange(() => {
expect(q.query.first.text).toEqual("1");
expect(q.query.last.text).toEqual("2");
async.done();
});
view.context.shouldShow = true;
view.detectChanges();
});
}));
it("should notify child's query before notifying parent's query",
inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => {
var template = '<needs-query-desc #q1>' +
'<needs-query-desc #q2>' +
'<div text="1"></div>' +
'</needs-query-desc>' +
'</needs-query-desc>';
tb.createView(MyComp, {html: template})
.then((view) => {
var q1 = view.rawView.locals.get("q1");
var q2 = view.rawView.locals.get("q2");
var firedQ2 = false;
q2.query.onChange(() => { firedQ2 = true; });
q1.query.onChange(() => {
expect(firedQ2).toBe(true);
async.done();
});
view.detectChanges();
});
}));
}); });
} }

View File

@ -51,7 +51,9 @@ class _MyComponent_ChangeDetector0 extends _gen.AbstractChangeDetector {
_alreadyChecked = true; _alreadyChecked = true;
} }
void callOnAllChangesDone() {} void callOnAllChangesDone() {
_dispatcher.notifyOnAllChangesDone();
}
void hydrate(MyComponent context, locals, directives) { void hydrate(MyComponent context, locals, directives) {
mode = 'ALWAYS_CHECK'; mode = 'ALWAYS_CHECK';

View File

@ -381,4 +381,5 @@ class FakeDirectives {
class DummyDispatcher extends ChangeDispatcher { class DummyDispatcher extends ChangeDispatcher {
notifyOnBinding(bindingRecord, newValue) { throw "Should not be used"; } notifyOnBinding(bindingRecord, newValue) { throw "Should not be used"; }
notifyOnAllChangesDone() {}
} }