diff --git a/modules/angular2/src/core/annotations/di.js b/modules/angular2/src/core/annotations/di.js index bf9ad88f06..34532d6e64 100644 --- a/modules/angular2/src/core/annotations/di.js +++ b/modules/angular2/src/core/annotations/di.js @@ -54,3 +54,15 @@ export class Attribute extends DependencyAnnotation { return this; } } + +/** + * The directive can inject an query that would reflect a list of ancestor directives + */ +export class Query extends DependencyAnnotation { + directive; + @CONST() + constructor(directive) { + super(); + this.directive = directive; + } +} diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index 1566d3a3cb..87301696fd 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -3,13 +3,14 @@ import {Math} from 'angular2/src/facade/math'; import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'angular2/di'; 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 * as viewModule from 'angular2/src/core/compiler/view'; import {ViewContainer} from 'angular2/src/core/compiler/view_container'; import {NgElement} from 'angular2/src/core/compiler/ng_element'; import {Directive, onChange, onDestroy, onAllChangesDone} from 'angular2/src/core/annotations/annotations'; import {BindingPropagationConfig} from 'angular2/change_detection'; import * as pclModule from 'angular2/src/core/compiler/private_component_location'; +import {QueryList} from './query_list'; var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10; @@ -41,39 +42,123 @@ class StaticKeys { } } -class TreeNode { +export class TreeNode { _parent:TreeNode; _head:TreeNode; _tail:TreeNode; _next:TreeNode; constructor(parent:TreeNode) { - this._parent = parent; this._head = null; this._tail = null; this._next = null; - if (isPresent(parent)) parent._addChild(this); + if (isPresent(parent)) parent.addChild(this); + } + + _assertConsistency() { + this._assertHeadBeforeTail(); + this._assertTailReachable(); + this._assertPresentInParentList(); + } + + _assertHeadBeforeTail() { + if (isBlank(this._tail) && isPresent(this._head)) throw new BaseException('null tail but non-null head'); + } + + _assertTailReachable() { + if (isBlank(this._tail)) return; + if (isPresent(this._tail._next)) throw new BaseException('node after tail'); + var p = this._head; + while (isPresent(p) && p != this._tail) p = p._next; + if (isBlank(p) && isPresent(this._tail)) throw new BaseException('tail not reachable.') + } + + _assertPresentInParentList() { + var p = this._parent; + if (isBlank(p)) { + return; + } + var cur = p._head; + while (isPresent(cur) && cur != this) cur = cur._next; + if (isBlank(cur)) throw new BaseException('node not reachable through parent.') } /** * Adds a child to the parent node. The child MUST NOT be a part of a tree. */ - _addChild(child:TreeNode) { + addChild(child:TreeNode) { if (isPresent(this._tail)) { this._tail._next = child; this._tail = child; } else { this._tail = this._head = child; } + child._next = null; + child._parent = this; + this._assertConsistency(); + } + + /** + * Adds a child to the parent node after a given sibling. + * The child MUST NOT be a part of a tree and the sibling must be present. + */ + addChildAfter(child:TreeNode, prevSibling:TreeNode) { + this._assertConsistency(); + if (isBlank(prevSibling)) { + var prevHead = this._head; + this._head = child; + child._next = prevHead; + if (isBlank(this._tail)) this._tail = child; + } else if (isBlank(prevSibling._next)) { + this.addChild(child); + return; + } else { + prevSibling._assertPresentInParentList(); + child._next = prevSibling._next; + prevSibling._next = child; + } + child._parent = this; + this._assertConsistency(); + } + + /** + * Detaches a node from the parent's tree. + */ + remove() { + this._assertConsistency(); + if (isBlank(this.parent)) return; + var nextSibling = this._next; + var prevSibling = this._findPrev(); + if (isBlank(prevSibling)) { + this.parent._head = this._next; + } else { + prevSibling._next = this._next; + } + if (isBlank(nextSibling)) { + this._parent._tail = prevSibling; + } + this._parent._assertConsistency(); + this._parent = null; + this._next = null; + this._assertConsistency(); + } + + /** + * Finds a previous sibling or returns null if first child. + * Assumes the node has a parent. + * TODO(rado): replace with DoublyLinkedList to avoid O(n) here. + */ + _findPrev() { + var node = this.parent._head; + if (node == this) return null; + while (node._next !== this) node = node._next; + return node; } get parent() { return this._parent; } - set parent(node:TreeNode) { - this._parent = node; - } - + // TODO(rado): replace with a function call, does too much work for a getter. get children() { var res = []; var child = this._head; @@ -90,14 +175,28 @@ export class DirectiveDependency extends Dependency { eventEmitterName:string; propSetterName:string; attributeName:string; + queryDirective; constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean, - properties:List, depth:int, eventEmitterName: string, propSetterName: string, attributeName:string) { + properties:List, depth:int, eventEmitterName: string, + propSetterName: string, attributeName:string, queryDirective) { super(key, asPromise, lazy, optional, properties); this.depth = depth; this.eventEmitterName = eventEmitterName; this.propSetterName = propSetterName; this.attributeName = attributeName; + this.queryDirective = queryDirective; + this._verify(); + } + + _verify() { + var count = 0; + if (isPresent(this.eventEmitterName)) count++; + if (isPresent(this.propSetterName)) count++; + if (isPresent(this.queryDirective)) count++; + if (isPresent(this.attributeName)) count++; + if (count > 1) throw new BaseException( + 'A directive injectable can contain only one of the following @EventEmitter, @PropertySetter, @Attribute or @Query.'); } static createFrom(d:Dependency):Dependency { @@ -106,6 +205,7 @@ export class DirectiveDependency extends Dependency { var propName = null; var attributeName = null; var properties = d.properties; + var queryDirective = null; for (var i = 0; i < properties.length; i++) { var property = properties[i]; @@ -119,11 +219,13 @@ export class DirectiveDependency extends Dependency { propName = property.propName; } else if (property instanceof Attribute) { attributeName = property.attributeName; + } else if (property instanceof Query) { + queryDirective = property.directive; } } return new DirectiveDependency(d.key, d.asPromise, d.lazy, d.optional, d.properties, depth, - eventName, propName, attributeName); + eventName, propName, attributeName, queryDirective); } } @@ -338,6 +440,11 @@ export class ElementInjector extends TreeNode { _privateComponent; _privateComponentBinding:DirectiveBinding; + // Queries are added during construction or linking with a new parent. + // They are never removed. + _query0: QueryRef; + _query1: QueryRef; + _query2: QueryRef; constructor(proto:ProtoElementInjector, parent:ElementInjector) { super(parent); this._proto = proto; @@ -358,6 +465,9 @@ export class ElementInjector extends TreeNode { this._obj8 = null; this._obj9 = null; this._constructionCounter = 0; + + this._inheritQueries(parent); + this._buildQueries(); } clearDirectives() { @@ -523,6 +633,8 @@ export class ElementInjector extends TreeNode { default: throw `Directive ${binding.key.token} can only have up to 10 dependencies.`; } + this._addToQueries(obj, binding.key.token); + return obj; } @@ -534,10 +646,11 @@ export class ElementInjector extends TreeNode { if (isPresent(dep.eventEmitterName)) return this._buildEventEmitter(dep); if (isPresent(dep.propSetterName)) return this._buildPropSetter(dep); if (isPresent(dep.attributeName)) return this._buildAttribute(dep); + if (isPresent(dep.queryDirective)) return this._findQuery(dep.queryDirective).list; return this._getByKey(dep.key, dep.depth, dep.optional, requestor); } - _buildEventEmitter(dep) { + _buildEventEmitter(dep: DirectiveDependency) { var view = this._getPreBuiltObjectByKeyId(StaticKeys.instance().viewId); return (event) => { view.triggerEventHandlers(dep.eventEmitterName, event, this._proto.index); @@ -562,6 +675,120 @@ export class ElementInjector extends TreeNode { } } + _buildQueriesForDeps(deps: List) { + for (var i = 0; i < deps.length; i++) { + var dep = deps[i]; + if (isPresent(dep.queryDirective)) { + this._createQueryRef(dep.queryDirective); + } + } + } + + _createQueryRef(directive) { + var queryList = new QueryList(); + if (isBlank(this._query0)) {this._query0 = new QueryRef(directive, queryList, this);} + else if (isBlank(this._query1)) {this._query1 = new QueryRef(directive, queryList, this);} + else if (isBlank(this._query2)) {this._query2 = new QueryRef(directive, queryList, this);} + else throw new QueryError(); + } + + _addToQueries(obj, token) { + if (isPresent(this._query0) && (this._query0.directive === token)) {this._query0.list.add(obj);} + if (isPresent(this._query1) && (this._query1.directive === token)) {this._query1.list.add(obj);} + if (isPresent(this._query2) && (this._query2.directive === token)) {this._query2.list.add(obj);} + } + + // TODO(rado): unify with _addParentQueries. + _inheritQueries(parent: ElementInjector) { + if (isBlank(parent)) return; + if (isPresent(parent._query0)) {this._query0 = parent._query0;} + if (isPresent(parent._query1)) {this._query1 = parent._query1;} + if (isPresent(parent._query2)) {this._query2 = parent._query2;} + } + + _buildQueries() { + if (isBlank(this._proto)) return; + var p = this._proto; + if (isPresent(p._binding0)) {this._buildQueriesForDeps(p._binding0.dependencies);} + if (isPresent(p._binding1)) {this._buildQueriesForDeps(p._binding1.dependencies);} + if (isPresent(p._binding2)) {this._buildQueriesForDeps(p._binding2.dependencies);} + if (isPresent(p._binding3)) {this._buildQueriesForDeps(p._binding3.dependencies);} + if (isPresent(p._binding4)) {this._buildQueriesForDeps(p._binding4.dependencies);} + if (isPresent(p._binding5)) {this._buildQueriesForDeps(p._binding5.dependencies);} + if (isPresent(p._binding6)) {this._buildQueriesForDeps(p._binding6.dependencies);} + if (isPresent(p._binding7)) {this._buildQueriesForDeps(p._binding7.dependencies);} + if (isPresent(p._binding8)) {this._buildQueriesForDeps(p._binding8.dependencies);} + if (isPresent(p._binding9)) {this._buildQueriesForDeps(p._binding9.dependencies);} + } + + _findQuery(token) { + if (isPresent(this._query0) && this._query0.directive === token) {return this._query0;} + if (isPresent(this._query1) && this._query1.directive === token) {return this._query1;} + if (isPresent(this._query2) && this._query2.directive === token) {return this._query2;} + throw new BaseException(`Cannot find query for directive ${token}.`); + } + + link(parent: ElementInjector) { + parent.addChild(this); + this._addParentQueries(); + } + + linkAfter(parent: ElementInjector, prevSibling: ElementInjector) { + parent.addChildAfter(this, prevSibling); + this._addParentQueries(); + } + + _addParentQueries() { + if (isPresent(this.parent._query0)) {this._addQueryToTree(this.parent._query0); this.parent._query0.update();} + if (isPresent(this.parent._query1)) {this._addQueryToTree(this.parent._query1); this.parent._query1.update();} + if (isPresent(this.parent._query2)) {this._addQueryToTree(this.parent._query2); this.parent._query2.update();} + } + + unlink() { + var queriesToUpDate = []; + if (isPresent(this.parent._query0)) {this._pruneQueryFromTree(this.parent._query0); ListWrapper.push(queriesToUpDate, this.parent._query0);} + if (isPresent(this.parent._query1)) {this._pruneQueryFromTree(this.parent._query1); ListWrapper.push(queriesToUpDate, this.parent._query1);} + if (isPresent(this.parent._query2)) {this._pruneQueryFromTree(this.parent._query2); ListWrapper.push(queriesToUpDate, this.parent._query2);} + + this.remove(); + + ListWrapper.forEach(queriesToUpDate, (q) => q.update()); + } + + + _pruneQueryFromTree(query: QueryRef) { + this._removeQueryRef(query); + + var child = this._head; + while (isPresent(child)) { + child._pruneQueryFromTree(query); + child = child._next; + } + } + + _addQueryToTree(query: QueryRef) { + this._assignQueryRef(query); + + var child = this._head; + while (isPresent(child)) { + child._addQueryToTree(query); + child = child._next; + } + } + + _assignQueryRef(query: QueryRef) { + if (isBlank(this._query0)) {this._query0 = query; return;} + else if (isBlank(this._query1)) {this._query1 = query; return;} + else if (isBlank(this._query2)) {this._query2 = query; return;} + throw new QueryError(); + } + + _removeQueryRef(query: QueryRef) { + if (this._query0 == query) this._query0 = null; + if (this._query1 == query) this._query1 = null; + if (this._query2 == query) this._query2 = null; + } + /* * It is fairly easy to annotate keys with metadata. * For example, key.metadata = 'directive'. @@ -700,3 +927,45 @@ class OutOfBoundsAccess extends Error { return this.message; } } + +class QueryError extends Error { + message:string; + // TODO(rado): pass the names of the active directives. + constructor() { + super(); + this.message = 'Only 3 queries can be concurrently active in a template.'; + } + + toString() { + return this.message; + } +} + +class QueryRef { + directive; + list: QueryList; + originator: ElementInjector; + constructor(directive, list: QueryList, originator: ElementInjector) { + this.directive = directive; + this.list = list; + this.originator = originator; + } + + update() { + var aggregator = []; + this.visit(this.originator, aggregator); + this.list.reset(aggregator); + } + + visit(inj: ElementInjector, aggregator) { + if (isBlank(inj)) return; + if (inj.hasDirective(this.directive)) { + ListWrapper.push(aggregator, inj.get(this.directive)); + } + var child = inj._head; + while (isPresent(child)) { + this.visit(child, aggregator); + child = child._next; + } + } +} diff --git a/modules/angular2/src/core/compiler/query_list.dart b/modules/angular2/src/core/compiler/query_list.dart new file mode 100644 index 0000000000..4faedcdc56 --- /dev/null +++ b/modules/angular2/src/core/compiler/query_list.dart @@ -0,0 +1,47 @@ +library angular2.src.core.compiler.query_list; + +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'dart:collection'; + +/** + * Injectable Objects that contains a live list of child directives in the light Dom of a directive. + * The directives are kept in depth-first pre-order traversal of the DOM. + * + * In the future this class will implement an Observable interface. + * For now it uses a plain list of observable callbacks. + */ +class QueryList extends Object with IterableMixin { + List _results; + List _callbacks; + bool _dirty; + + QueryList(): _results = [], _callbacks = [], _dirty = false; + + Iterator get iterator => _results.iterator; + + reset(newList) { + _results = newList; + _dirty = true; + } + + add(obj) { + _results.add(obj); + _dirty = true; + } + + // TODO(rado): hook up with change detection after #995. + fireCallbacks() { + if (_dirty) { + _callbacks.forEach((c) => c()); + _dirty = false; + } + } + + onChange(callback) { + this._callbacks.add(callback); + } + + removeCallback(callback) { + this._callbacks.remove(callback); + } +} diff --git a/modules/angular2/src/core/compiler/query_list.es6 b/modules/angular2/src/core/compiler/query_list.es6 new file mode 100644 index 0000000000..628374f181 --- /dev/null +++ b/modules/angular2/src/core/compiler/query_list.es6 @@ -0,0 +1,51 @@ +import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; +import {Directive} from 'angular2/src/core/annotations/annotations'; + +/** + * Injectable Objects that contains a live list of child directives in the light Dom of a directive. + * The directives are kept in depth-first pre-order traversal of the DOM. + * + * In the future this class will implement an Observable interface. + * For now it uses a plain list of observable callbacks. + */ +export class QueryList { + _results: List; + _callbacks; + _dirty; + + constructor() { + this._results = []; + this._callbacks = []; + this._dirty = false; + } + + [Symbol.iterator]() { + return this._results[Symbol.iterator](); + } + + reset(newList) { + this._results = newList; + this._dirty = true; + } + + add(obj) { + ListWrapper.push(this._results, obj); + this._dirty = true; + } + + // TODO(rado): hook up with change detection after #995. + fireCallbacks() { + if (this._dirty) { + ListWrapper.forEach(this._callbacks, (c) => c()); + this._dirty = false; + } + } + + onChange(callback) { + ListWrapper.push(this._callbacks, callback); + } + + removeCallback(callback) { + ListWrapper.remove(this._callbacks, callback); + } +} diff --git a/modules/angular2/src/core/compiler/view_container.js b/modules/angular2/src/core/compiler/view_container.js index 3d11a85c7f..26f626b74f 100644 --- a/modules/angular2/src/core/compiler/view_container.js +++ b/modules/angular2/src/core/compiler/view_container.js @@ -75,6 +75,11 @@ export class ViewContainer { return this._views.length; } + _siblingInjectorToLinkAfter(index: number) { + if (index == 0) return null; + return ListWrapper.last(this._views[index - 1].rootElementInjectors) + } + hydrated() { return isPresent(this.appInjector); } @@ -106,7 +111,7 @@ export class ViewContainer { if (atIndex == -1) atIndex = this._views.length; ListWrapper.insert(this._views, atIndex, view); this.parentView.changeDetector.addChild(view.changeDetector); - this._linkElementInjectors(view); + this._linkElementInjectors(this._siblingInjectorToLinkAfter(atIndex), view); return view; } @@ -133,15 +138,19 @@ export class ViewContainer { return detachedView; } - _linkElementInjectors(view) { - for (var i = 0; i < view.rootElementInjectors.length; ++i) { - view.rootElementInjectors[i].parent = this.elementInjector; + contentTagContainers() { + return this._views; + } + + _linkElementInjectors(sibling, view) { + for (var i = view.rootElementInjectors.length - 1; i >= 0; i--) { + view.rootElementInjectors[i].linkAfter(this.elementInjector, sibling); } } _unlinkElementInjectors(view) { for (var i = 0; i < view.rootElementInjectors.length; ++i) { - view.rootElementInjectors[i].parent = null; + view.rootElementInjectors[i].unlink(); } } } diff --git a/modules/angular2/src/render/dom/view/view_container.js b/modules/angular2/src/render/dom/view/view_container.js index 570fceb1c2..3e164c7aa5 100644 --- a/modules/angular2/src/render/dom/view/view_container.js +++ b/modules/angular2/src/render/dom/view/view_container.js @@ -122,6 +122,7 @@ export class ViewContainer { return r; } + static moveViewNodesAfterSibling(sibling, view) { for (var i = view.rootNodes.length - 1; i >= 0; --i) { DOM.insertAfter(sibling, view.rootNodes[i]); diff --git a/modules/angular2/test/core/compiler/element_injector_spec.js b/modules/angular2/test/core/compiler/element_injector_spec.js index 8c9f918733..b2b46a28ce 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.js +++ b/modules/angular2/test/core/compiler/element_injector_spec.js @@ -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]); } -} \ No newline at end of file +} diff --git a/modules/angular2/test/core/compiler/query_integration_spec.js b/modules/angular2/test/core/compiler/query_integration_spec.js new file mode 100644 index 0000000000..2937802f49 --- /dev/null +++ b/modules/angular2/test/core/compiler/query_integration_spec.js @@ -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 = + '
' + + '
' + + '
'; + + 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 = + '
' + + '
' + + '
'; + + 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 = + '
' + + '
' + + '
'; + + 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: '
{{dir.text}}|
' +}) +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']; + } +} diff --git a/modules/angular2/test/core/compiler/query_list_spec.js b/modules/angular2/test/core/compiler/query_list_spec.js new file mode 100644 index 0000000000..07f2fbd441 --- /dev/null +++ b/modules/angular2/test/core/compiler/query_list_spec.js @@ -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); + }); + }); + }); +}