feat(query): adds initial implementation of the query api.
Queries allow a directive to inject a live list of directives of a given type from its LightDom. The injected list is Iterable (in JS and Dart). It will be Observable when Observables are support in JS, for now it maintains a simple list of onChange callbacks API. To support queries, element injectors now maintain a list of child injectors in the correct DOM order (dynamically updated by viewports). For performance reasons we allow only 3 active queries in an injector subtree. The feature adds no overhead to the application when not used. Queries walk the injector tree only during dynamic view addition/removal as triggered by viewport directives. Syncs changes between viewContainer on the render and logic sides. Closes #792
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, SpyObject, proxy, el} from 'angular2/test_lib';
|
||||
import {isBlank, isPresent, IMPLEMENTS} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, MapWrapper, List, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
|
||||
import {ListWrapper, MapWrapper, List, StringMapWrapper, iterateListLike} from 'angular2/src/facade/collection';
|
||||
import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding, TreeNode} from 'angular2/src/core/compiler/element_injector';
|
||||
import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility';
|
||||
import {EventEmitter, PropertySetter, Attribute} from 'angular2/src/core/annotations/di';
|
||||
import {EventEmitter, PropertySetter, Attribute, Query} from 'angular2/src/core/annotations/di';
|
||||
import {onDestroy} from 'angular2/src/core/annotations/annotations';
|
||||
import {Optional, Injector, Inject, bind} from 'angular2/di';
|
||||
import {ProtoView, View} from 'angular2/src/core/compiler/view';
|
||||
@ -13,6 +13,7 @@ import {Directive} from 'angular2/src/core/annotations/annotations';
|
||||
import {BindingPropagationConfig, Parser, Lexer} from 'angular2/change_detection';
|
||||
|
||||
import {ViewRef, Renderer} from 'angular2/src/render/api';
|
||||
import {QueryList} from 'angular2/src/core/compiler/query_list';
|
||||
|
||||
class DummyDirective extends Directive {
|
||||
constructor({lifecycle} = {}) { super({lifecycle: lifecycle}); }
|
||||
@ -26,10 +27,24 @@ class DummyView extends SpyObject {noSuchMethod(m){super.noSuchMethod(m)}}
|
||||
class SimpleDirective {
|
||||
}
|
||||
|
||||
|
||||
class SomeOtherDirective {
|
||||
}
|
||||
|
||||
var _constructionCount = 0;
|
||||
class CountingDirective {
|
||||
count;
|
||||
constructor() {
|
||||
this.count = _constructionCount;
|
||||
_constructionCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
class FancyCountingDirective extends CountingDirective {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
class NeedsDirective {
|
||||
dependency:SimpleDirective;
|
||||
constructor(dependency:SimpleDirective){
|
||||
@ -148,6 +163,13 @@ class NeedsAttributeNoType {
|
||||
}
|
||||
}
|
||||
|
||||
class NeedsQuery {
|
||||
query: QueryList;
|
||||
constructor(@Query(CountingDirective) query: QueryList) {
|
||||
this.query = query;
|
||||
}
|
||||
}
|
||||
|
||||
class A_Needs_B {
|
||||
constructor(dep){}
|
||||
}
|
||||
@ -175,6 +197,17 @@ class DirectiveWithDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
class TestNode extends TreeNode {
|
||||
message: string;
|
||||
constructor(parent:TestNode, message) {
|
||||
super(parent);
|
||||
this.message = message;
|
||||
}
|
||||
toString() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
export function main() {
|
||||
var defaultPreBuiltObjects = new PreBuiltObjects(null, null, null, null);
|
||||
var appInjector = new Injector([]);
|
||||
@ -235,6 +268,81 @@ export function main() {
|
||||
return shadow;
|
||||
}
|
||||
|
||||
describe('TreeNodes', () => {
|
||||
var root, firstParent, lastParent, node;
|
||||
|
||||
/*
|
||||
Build a tree of the following shape:
|
||||
root
|
||||
- p1
|
||||
- c1
|
||||
- c2
|
||||
- p2
|
||||
- c3
|
||||
*/
|
||||
beforeEach(() => {
|
||||
root = new TestNode(null, 'root');
|
||||
var p1 = firstParent = new TestNode(root, 'p1');
|
||||
var p2 = lastParent = new TestNode(root, 'p2');
|
||||
node = new TestNode(p1, 'c1');
|
||||
new TestNode(p1, 'c2');
|
||||
new TestNode(p2, 'c3');
|
||||
});
|
||||
|
||||
// depth-first pre-order.
|
||||
function walk(node, f) {
|
||||
if (isBlank(node)) return f;
|
||||
f(node);
|
||||
ListWrapper.forEach(node.children, (n) => walk(n, f));
|
||||
}
|
||||
|
||||
function logWalk(node) {
|
||||
var log = '';
|
||||
walk(node, (n) => {
|
||||
log += (log.length != 0 ? ', ' : '') + n.toString();
|
||||
});
|
||||
return log;
|
||||
}
|
||||
|
||||
it('should support listing children', () => {
|
||||
expect(logWalk(root)).toEqual('root, p1, c1, c2, p2, c3');
|
||||
});
|
||||
|
||||
it('should support removing the first child node', () => {
|
||||
firstParent.remove();
|
||||
|
||||
expect(firstParent.parent).toEqual(null);
|
||||
expect(logWalk(root)).toEqual('root, p2, c3');
|
||||
});
|
||||
|
||||
it('should support removing the last child node', () => {
|
||||
lastParent.remove();
|
||||
|
||||
expect(logWalk(root)).toEqual('root, p1, c1, c2');
|
||||
});
|
||||
|
||||
it('should support moving a node at the end of children', () => {
|
||||
node.remove();
|
||||
root.addChild(node);
|
||||
|
||||
expect(logWalk(root)).toEqual('root, p1, c2, p2, c3, c1');
|
||||
});
|
||||
|
||||
it('should support moving a node in the beginning of children', () => {
|
||||
node.remove();
|
||||
lastParent.addChildAfter(node, null);
|
||||
|
||||
expect(logWalk(root)).toEqual('root, p1, c2, p2, c1, c3');
|
||||
});
|
||||
|
||||
it('should support moving a node in the middle of children', () => {
|
||||
node.remove();
|
||||
lastParent.addChildAfter(node, firstParent);
|
||||
|
||||
expect(logWalk(root)).toEqual('root, p1, c2, c1, p2, c3');
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProtoElementInjector", () => {
|
||||
describe("direct parent", () => {
|
||||
it("should return parent proto injector when distance is 1", () => {
|
||||
@ -374,7 +482,7 @@ export function main() {
|
||||
});
|
||||
|
||||
it("should not instantiate directives that depend on other directives in the containing component's ElementInjector", () => {
|
||||
expect( () => {
|
||||
expect(() => {
|
||||
hostShadowInjectors([SomeOtherDirective, SimpleDirective], [NeedsDirective]);
|
||||
}).toThrowError('No provider for SimpleDirective! (NeedsDirective -> SimpleDirective)')
|
||||
});
|
||||
@ -394,7 +502,7 @@ export function main() {
|
||||
var shadowAppInjector = new Injector([
|
||||
bind("service").toValue("service")
|
||||
]);
|
||||
expect( () => {
|
||||
expect(() => {
|
||||
injector([SomeOtherDirective, NeedsService], null, shadowAppInjector);
|
||||
}).toThrowError('No provider for service! (NeedsService -> service)');
|
||||
});
|
||||
@ -434,7 +542,7 @@ export function main() {
|
||||
|
||||
it("should throw when no SimpleDirective found", function () {
|
||||
expect(() => injector([NeedDirectiveFromParent])).
|
||||
toThrowError('No provider for SimpleDirective! (NeedDirectiveFromParent -> SimpleDirective)');
|
||||
toThrowError('No provider for SimpleDirective! (NeedDirectiveFromParent -> SimpleDirective)');
|
||||
});
|
||||
|
||||
it("should inject null when no directive found", function () {
|
||||
@ -470,7 +578,7 @@ export function main() {
|
||||
DirectiveBinding.createFromBinding(bBneedsA, null)
|
||||
]);
|
||||
}).toThrowError('Cannot instantiate cyclic dependency! ' +
|
||||
'(A_Needs_B -> B_Needs_A -> A_Needs_B)');
|
||||
'(A_Needs_B -> B_Needs_A -> A_Needs_B)');
|
||||
});
|
||||
|
||||
it("should call onDestroy on directives subscribed to this event", function() {
|
||||
@ -675,6 +783,132 @@ export function main() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('directive queries', () => {
|
||||
var preBuildObjects = defaultPreBuiltObjects;
|
||||
beforeEach(() => {
|
||||
_constructionCount = 0;
|
||||
});
|
||||
|
||||
function expectDirectives(query, type, expectedIndex) {
|
||||
var currentCount = 0;
|
||||
iterateListLike(query, (i) => {
|
||||
expect(i).toBeAnInstanceOf(type);
|
||||
expect(i.count).toBe(expectedIndex[currentCount]);
|
||||
currentCount += 1;
|
||||
});
|
||||
}
|
||||
|
||||
it('should be injectable', () => {
|
||||
var inj = injector([NeedsQuery], null, null, preBuildObjects);
|
||||
expect(inj.get(NeedsQuery).query).toBeAnInstanceOf(QueryList);
|
||||
});
|
||||
|
||||
it('should contain directives on the same injector', () => {
|
||||
var inj = injector([NeedsQuery, CountingDirective], null, null, preBuildObjects);
|
||||
|
||||
expectDirectives(inj.get(NeedsQuery).query, CountingDirective, [0]);
|
||||
});
|
||||
|
||||
// Dart's restriction on static types in (a is A) makes this feature hard to implement.
|
||||
// Current proposal is to add second parameter the Query constructor to take a
|
||||
// comparison function to support user-defined definition of matching.
|
||||
|
||||
//it('should support super class directives', () => {
|
||||
// var inj = injector([NeedsQuery, FancyCountingDirective], null, null, preBuildObjects);
|
||||
//
|
||||
// expectDirectives(inj.get(NeedsQuery).query, FancyCountingDirective, [0]);
|
||||
//});
|
||||
|
||||
it('should contain directives on the same and a child injector in construction order', () => {
|
||||
var protoParent = new ProtoElementInjector(null, 0, [NeedsQuery, CountingDirective]);
|
||||
var protoChild = new ProtoElementInjector(protoParent, 1, [CountingDirective]);
|
||||
|
||||
var parent = protoParent.instantiate(null);
|
||||
var child = protoChild.instantiate(parent);
|
||||
parent.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
child.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
|
||||
expectDirectives(parent.get(NeedsQuery).query, CountingDirective, [0,1]);
|
||||
});
|
||||
|
||||
it('should reflect unlinking an injector', () => {
|
||||
var protoParent = new ProtoElementInjector(null, 0, [NeedsQuery, CountingDirective]);
|
||||
var protoChild = new ProtoElementInjector(protoParent, 1, [CountingDirective]);
|
||||
|
||||
var parent = protoParent.instantiate(null);
|
||||
var child = protoChild.instantiate(parent);
|
||||
parent.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
child.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
|
||||
child.unlink();
|
||||
|
||||
expectDirectives(parent.get(NeedsQuery).query, CountingDirective, [0]);
|
||||
});
|
||||
|
||||
it('should reflect moving an injector as a last child', () => {
|
||||
var protoParent = new ProtoElementInjector(null, 0, [NeedsQuery, CountingDirective]);
|
||||
var protoChild1 = new ProtoElementInjector(protoParent, 1, [CountingDirective]);
|
||||
var protoChild2 = new ProtoElementInjector(protoParent, 1, [CountingDirective]);
|
||||
|
||||
var parent = protoParent.instantiate(null);
|
||||
var child1 = protoChild1.instantiate(parent);
|
||||
var child2 = protoChild2.instantiate(parent);
|
||||
|
||||
parent.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
child1.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
child2.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
|
||||
child1.unlink();
|
||||
child1.link(parent);
|
||||
|
||||
var queryList = parent.get(NeedsQuery).query;
|
||||
expectDirectives(queryList, CountingDirective, [0, 2, 1]);
|
||||
});
|
||||
|
||||
it('should reflect moving an injector as a first child', () => {
|
||||
var protoParent = new ProtoElementInjector(null, 0, [NeedsQuery, CountingDirective]);
|
||||
var protoChild1 = new ProtoElementInjector(protoParent, 1, [CountingDirective]);
|
||||
var protoChild2 = new ProtoElementInjector(protoParent, 1, [CountingDirective]);
|
||||
|
||||
var parent = protoParent.instantiate(null);
|
||||
var child1 = protoChild1.instantiate(parent);
|
||||
var child2 = protoChild2.instantiate(parent);
|
||||
|
||||
parent.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
child1.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
child2.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
|
||||
child2.unlink();
|
||||
child2.linkAfter(parent, null);
|
||||
|
||||
var queryList = parent.get(NeedsQuery).query;
|
||||
expectDirectives(queryList, CountingDirective, [0, 2, 1]);
|
||||
});
|
||||
|
||||
it('should support two concurrent queries for the same directive', () => {
|
||||
var protoGrandParent = new ProtoElementInjector(null, 0, [NeedsQuery]);
|
||||
var protoParent = new ProtoElementInjector(null, 0, [NeedsQuery]);
|
||||
var protoChild = new ProtoElementInjector(protoParent, 1, [CountingDirective]);
|
||||
|
||||
var grandParent = protoGrandParent.instantiate(null);
|
||||
var parent = protoParent.instantiate(grandParent);
|
||||
var child = protoChild.instantiate(parent);
|
||||
|
||||
grandParent.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
parent.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
child.instantiateDirectives(new Injector([]), null, null, preBuildObjects);
|
||||
|
||||
var queryList1 = grandParent.get(NeedsQuery).query;
|
||||
var queryList2 = parent.get(NeedsQuery).query;
|
||||
|
||||
expectDirectives(queryList1, CountingDirective, [0]);
|
||||
expectDirectives(queryList2, CountingDirective, [0]);
|
||||
|
||||
child.unlink();
|
||||
expectDirectives(queryList1, CountingDirective, []);
|
||||
expectDirectives(queryList2, CountingDirective, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -695,4 +929,4 @@ class FakeRenderer extends Renderer {
|
||||
ListWrapper.push(this.log, [viewRef, elementIndex, propertyName, value]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
122
modules/angular2/test/core/compiler/query_integration_spec.js
vendored
Normal file
122
modules/angular2/test/core/compiler/query_integration_spec.js
vendored
Normal file
@ -0,0 +1,122 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
IS_NODEJS,
|
||||
it,
|
||||
xit,
|
||||
} from 'angular2/test_lib';
|
||||
|
||||
import {TestBed} from 'angular2/src/test_lib/test_bed';
|
||||
|
||||
import {QueryList} from 'angular2/src/core/compiler/query_list';
|
||||
import {Query} from 'angular2/src/core/annotations/di';
|
||||
|
||||
import {Decorator, Component, Template, If, For} from 'angular2/angular2';
|
||||
|
||||
import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter';
|
||||
|
||||
export function main() {
|
||||
BrowserDomAdapter.makeCurrent();
|
||||
describe('Query API', () => {
|
||||
|
||||
it('should contain all directives in the light dom', inject([TestBed, AsyncTestCompleter], (tb, async) => {
|
||||
var template =
|
||||
'<div text="1"></div>' +
|
||||
'<needs-query text="2"><div text="3"></div></needs-query>' +
|
||||
'<div text="4"></div>';
|
||||
|
||||
tb.createView(MyComp, {html: template}).then((view) => {
|
||||
view.detectChanges();
|
||||
expect(view.rootNodes).toHaveText('2|3|');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should reflect dynamically inserted directives', inject([TestBed, AsyncTestCompleter], (tb, async) => {
|
||||
var template =
|
||||
'<div text="1"></div>' +
|
||||
'<needs-query text="2"><div *if="shouldShow" [text]="\'3\'"></div></needs-query>' +
|
||||
'<div text="4"></div>';
|
||||
|
||||
tb.createView(MyComp, {html: template}).then((view) => {
|
||||
|
||||
view.detectChanges();
|
||||
expect(view.rootNodes).toHaveText('2|');
|
||||
|
||||
view.context.shouldShow = true;
|
||||
view.detectChanges();
|
||||
// TODO(rado): figure out why the second tick is necessary.
|
||||
view.detectChanges();
|
||||
expect(view.rootNodes).toHaveText('2|3|');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should reflect moved directives', inject([TestBed, AsyncTestCompleter], (tb, async) => {
|
||||
var template =
|
||||
'<div text="1"></div>' +
|
||||
'<needs-query text="2"><div *for="var i of list" [text]="i"></div></needs-query>' +
|
||||
'<div text="4"></div>';
|
||||
|
||||
tb.createView(MyComp, {html: template}).then((view) => {
|
||||
view.detectChanges();
|
||||
view.detectChanges();
|
||||
|
||||
expect(view.rootNodes).toHaveText('2|1d|2d|3d|');
|
||||
|
||||
view.context.list = ['3d', '2d'];
|
||||
view.detectChanges();
|
||||
view.detectChanges();
|
||||
expect(view.rootNodes).toHaveText('2|3d|2d|');
|
||||
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'needs-query'})
|
||||
@Template({
|
||||
directives: [For],
|
||||
inline: '<div *for="var dir of query">{{dir.text}}|</div>'
|
||||
})
|
||||
class NeedsQuery {
|
||||
query: QueryList;
|
||||
constructor(@Query(TextDirective) query: QueryList) {
|
||||
this.query = query;
|
||||
}
|
||||
}
|
||||
|
||||
var _constructiontext = 0;
|
||||
|
||||
@Decorator({
|
||||
selector: '[text]',
|
||||
bind: {
|
||||
'text': 'text'
|
||||
}
|
||||
})
|
||||
class TextDirective {
|
||||
text: string;
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
@Component({selector: 'my-comp'})
|
||||
@Template({
|
||||
directives: [NeedsQuery, TextDirective, If, For]
|
||||
})
|
||||
class MyComp {
|
||||
shouldShow: boolean;
|
||||
list;
|
||||
constructor() {
|
||||
this.shouldShow = false;
|
||||
this.list = ['1d', '2d', '3d'];
|
||||
}
|
||||
}
|
69
modules/angular2/test/core/compiler/query_list_spec.js
vendored
Normal file
69
modules/angular2/test/core/compiler/query_list_spec.js
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
import {describe, it, expect, beforeEach, ddescribe, iit, xit, el} from 'angular2/test_lib';
|
||||
|
||||
import {List, MapWrapper, ListWrapper, iterateListLike} from 'angular2/src/facade/collection';
|
||||
import {QueryList} from 'angular2/src/core/compiler/query_list';
|
||||
|
||||
|
||||
export function main() {
|
||||
describe('QueryList', () => {
|
||||
var queryList, log;
|
||||
beforeEach(() => {
|
||||
queryList = new QueryList();
|
||||
log = '';
|
||||
});
|
||||
|
||||
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');
|
||||
iterateListLike(queryList, logAppend);
|
||||
expect(log).toEqual('one again, two again');
|
||||
});
|
||||
|
||||
describe('simple observable interface', () => {
|
||||
it('should fire callbacks on change', () => {
|
||||
var fires = 0;
|
||||
queryList.onChange(() => {fires += 1;});
|
||||
|
||||
queryList.fireCallbacks();
|
||||
expect(fires).toEqual(0);
|
||||
|
||||
queryList.add('one');
|
||||
|
||||
queryList.fireCallbacks();
|
||||
expect(fires).toEqual(1);
|
||||
|
||||
queryList.fireCallbacks();
|
||||
expect(fires).toEqual(1);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user