diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index 19b80f6c2b..0783067dcf 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -76,90 +76,19 @@ export class StaticKeys { export class TreeNode> { _parent: T; - _head: T = null; - _tail: T = null; - _next: T = null; constructor(parent: T) { - if (isPresent(parent)) parent.addChild(this); - } - - /** - * Adds a child to the parent node. The child MUST NOT be a part of a tree. - */ - addChild(child: T): void { - if (isPresent(this._tail)) { - this._tail._next = child; - this._tail = child; + if (isPresent(parent)) { + parent.addChild(this); } else { - this._tail = this._head = child; + this._parent = null; } - child._next = null; - child._parent = this; } - /** - * 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: T, prevSibling: T): void { - 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 { - child._next = prevSibling._next; - prevSibling._next = child; - } - child._parent = this; - } + addChild(child: T): void { child._parent = this; } - /** - * Detaches a node from the parent's tree. - */ - remove(): void { - 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 = null; - this._next = null; - } - - /** - * 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; - } + remove(): void { this._parent = null; } get parent() { return this._parent; } - - // TODO(rado): replace with a function call, does too much work for a getter. - get children(): T[] { - var res = []; - var child = this._head; - while (child != null) { - res.push(child); - child = child._next; - } - return res; - } } export class DirectiveDependency extends Dependency { @@ -395,8 +324,7 @@ export class ElementInjector extends TreeNode implements Depend private _host: ElementInjector; private _preBuiltObjects: PreBuiltObjects = null; - // Queries are added during construction or linking with a new parent. - // They are removed only through unlinking. + // QueryRefs are added during construction. They are never removed. private _query0: QueryRef; private _query1: QueryRef; private _query2: QueryRef; @@ -421,7 +349,6 @@ export class ElementInjector extends TreeNode implements Depend this.hydrated = false; this._buildQueries(); - this._addParentQueries(); } dehydrate(): void { @@ -433,49 +360,44 @@ export class ElementInjector extends TreeNode implements Depend this._clearQueryLists(); } - afterContentChecked(): 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(imperativelyCreatedInjector: Injector, host: ElementInjector, preBuiltObjects: PreBuiltObjects): void { this._host = host; this._preBuiltObjects = preBuiltObjects; - if (isPresent(host)) { - this._addViewQueries(host); - } - this._reattachInjectors(imperativelyCreatedInjector); this._strategy.hydrate(); - this._addDirectivesToQueries(); - this._addVarBindingsToQueries(); - this.hydrated = true; - - // TODO(rado): optimize this call, if view queries are not moved around, - // simply appending to the query list is faster than updating. - this._updateViewQueries(); } - private _updateViewQueries() { + updateLocalQueries() { + 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(); + } + } + + updateLocalViewQueries() { 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(); } } @@ -554,6 +476,8 @@ export class ElementInjector extends TreeNode implements Depend return new ViewContainerRef(this._preBuiltObjects.viewManager, this.getElementRef()); } + getView(): viewModule.AppView { return this._preBuiltObjects.view; } + directParent(): ElementInjector { return this._proto.distanceToParent < 2 ? this.parent : null; } isComponentKey(key: Key): boolean { return this._strategy.isComponentKey(key); } @@ -633,54 +557,6 @@ export class ElementInjector extends TreeNode implements Depend } } - private _addViewQueries(host: ElementInjector): void { - this._addViewQuery(host._query0, host); - this._addViewQuery(host._query1, host); - this._addViewQuery(host._query2, host); - } - - private _addViewQuery(queryRef: QueryRef, host: ElementInjector): void { - if (isBlank(queryRef) || !queryRef.isViewQuery || this._hasQuery(queryRef)) return; - if (queryRef.originator == host) { - // TODO(rado): Replace this.parent check with distanceToParent = 1 when - // https://github.com/angular/angular/issues/2707 is fixed. - if (!queryRef.query.descendants && isPresent(this.parent)) return; - this._assignQueryRef(queryRef); - } - } - - private _addVarBindingsToQueries(): void { - this._addVarBindingsToQuery(this._query0); - this._addVarBindingsToQuery(this._query1); - this._addVarBindingsToQuery(this._query2); - } - - private _addDirectivesToQueries(): void { - this._addDirectivesToQuery(this._query0); - this._addDirectivesToQuery(this._query1); - this._addDirectivesToQuery(this._query2); - } - - private _addVarBindingsToQuery(queryRef: QueryRef): void { - if (isBlank(queryRef) || !queryRef.query.isVarBindingQuery) return; - - var vb = queryRef.query.varBindings; - for (var i = 0; i < vb.length; ++i) { - if (this.hasVariableBinding(vb[i])) { - queryRef.list.add(this.getVariableBinding(vb[i])); - } - } - } - - private _addDirectivesToQuery(queryRef: QueryRef): void { - if (isBlank(queryRef) || queryRef.query.isVarBindingQuery) return; - if (queryRef.isViewQuery && queryRef.originator == this) return; - - var matched = []; - this.addDirectivesMatchingQuery(queryRef.query, matched); - matched.forEach(s => queryRef.list.add(s)); - } - private _createQueryRef(query: QueryMetadata): void { var queryList = new QueryList(); if (isBlank(this._query0)) { @@ -695,7 +571,7 @@ export class ElementInjector extends TreeNode implements Depend } addDirectivesMatchingQuery(query: QueryMetadata, list: any[]): void { - var templateRef = this._preBuiltObjects.templateRef; + var templateRef = isBlank(this._preBuiltObjects) ? null : this._preBuiltObjects.templateRef; if (query.selector === TemplateRef && isPresent(templateRef)) { list.push(templateRef); } @@ -721,105 +597,9 @@ export class ElementInjector extends TreeNode implements Depend throw new BaseException(`Cannot find query for directive ${query}.`); } - _hasQuery(query: QueryRef): boolean { - return this._query0 == query || this._query1 == query || this._query2 == query; - } + link(parent: ElementInjector): void { parent.addChild(this); } - link(parent: ElementInjector): void { - parent.addChild(this); - this._addParentQueries(); - } - - linkAfter(parent: ElementInjector, prevSibling: ElementInjector): void { - parent.addChildAfter(this, prevSibling); - this._addParentQueries(); - } - - unlink(): void { - var parent = this.parent; - this.remove(); - this._removeParentQueries(parent); - } - - private _addParentQueries(): void { - if (isBlank(this.parent)) return; - this._addParentQuery(this.parent._query0); - this._addParentQuery(this.parent._query1); - this._addParentQuery(this.parent._query2); - } - - private _addParentQuery(query): void { - if (isPresent(query) && !this._hasQuery(query)) { - this._addQueryToTree(query); - if (this.hydrated) query.update(); - } - } - - private _removeParentQueries(parent: ElementInjector): void { - this._removeParentQuery(parent._query0); - this._removeParentQuery(parent._query1); - this._removeParentQuery(parent._query2); - } - - private _removeParentQuery(query: QueryRef) { - if (isPresent(query)) { - this._pruneQueryFromTree(query); - query.update(); - } - } - - private _pruneQueryFromTree(query: QueryRef): void { - this._removeQueryRef(query); - - var child = this._head; - while (isPresent(child)) { - child._pruneQueryFromTree(query); - child = child._next; - } - } - - private _addQueryToTree(queryRef: QueryRef): void { - if (queryRef.query.descendants == false) { - if (this == queryRef.originator) { - this._addQueryToTreeSelfAndRecurse(queryRef); - // TODO(rado): add check for distance to parent = 1 when issue #2707 is fixed. - } else if (this.parent == queryRef.originator) { - this._assignQueryRef(queryRef); - } - } else { - this._addQueryToTreeSelfAndRecurse(queryRef); - } - } - - private _addQueryToTreeSelfAndRecurse(queryRef: QueryRef): void { - this._assignQueryRef(queryRef); - - var child = this._head; - while (isPresent(child)) { - child._addQueryToTree(queryRef); - child = child._next; - } - } - - private _assignQueryRef(query: QueryRef): void { - 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(); - } - - private _removeQueryRef(query: QueryRef): void { - if (this._query0 == query) this._query0 = null; - if (this._query1 == query) this._query1 = null; - if (this._query2 == query) this._query2 = null; - } + unlink(): void { this.remove(); } getDirectiveAtIndex(index: number): any { return this._injector.getAt(index); } @@ -837,9 +617,34 @@ export class ElementInjector extends TreeNode implements Depend } private _clearQueryLists(): void { - if (isPresent(this._query0) && this._query0.originator === this) this._query0.reset(); - if (isPresent(this._query1) && this._query1.originator === this) this._query1.reset(); - if (isPresent(this._query2) && this._query2.originator === this) this._query2.reset(); + if (isPresent(this._query0)) this._query0.reset(); + if (isPresent(this._query1)) this._query1.reset(); + if (isPresent(this._query2)) this._query2.reset(); + } + + afterViewChecked(): void { this.updateLocalViewQueries(); } + + afterContentChecked(): void { this.updateLocalQueries(); } + + traverseAndSetQueriesAsDirty(): void { + var inj = this; + while (isPresent(inj)) { + inj._setQueriesAsDirty(); + inj = inj.parent; + } + } + + private _setQueriesAsDirty(): void { + if (isPresent(this._query0) && !this._query0.isViewQuery) this._query0.dirty = true; + if (isPresent(this._query1) && !this._query1.isViewQuery) this._query1.dirty = true; + if (isPresent(this._query2) && !this._query2.isViewQuery) this._query2.dirty = true; + if (isPresent(this._host)) this._host._setViewQueriesAsDirty(); + } + + private _setViewQueriesAsDirty(): void { + if (isPresent(this._query0) && this._query0.isViewQuery) this._query0.dirty = true; + if (isPresent(this._query1) && this._query1.isViewQuery) this._query1.dirty = true; + if (isPresent(this._query2) && this._query2.isViewQuery) this._query2.dirty = true; } } @@ -1113,7 +918,7 @@ export class QueryError extends BaseException { // TODO(rado): pass the names of the active directives. constructor() { super(); - this.message = 'Only 3 queries can be concurrently active in a template.'; + this.message = 'Only 3 queries can be concurrently active on an element.'; } toString(): string { return this.message; } @@ -1121,37 +926,80 @@ export class QueryError extends BaseException { export class QueryRef { constructor(public query: QueryMetadata, public list: QueryList, - public originator: ElementInjector) {} + public originator: ElementInjector, public dirty: boolean = true) {} get isViewQuery(): boolean { return this.query.isViewQuery; } update(): void { - var aggregator = []; - if (this.query.isViewQuery) { - // intentionally skipping originator for view queries. - var rootViewInjectors = this.originator.getRootViewInjectors(); - for (var i = 0; i < rootViewInjectors.length; i++) { - this.visit(rootViewInjectors[i], aggregator); - } - } else { - this.visit(this.originator, aggregator); - } - this.list.reset(aggregator); + if (!this.dirty) return; + this._update(); + this.dirty = false; } - visit(inj: ElementInjector, aggregator: any[]): void { - if (isBlank(inj) || !inj._hasQuery(this) || !inj.hydrated) return; + private _update(): void { + var aggregator = []; + if (this.query.isViewQuery) { + var view = this.originator.getView(); + // intentionally skipping originator for view queries. + var nestedView = + view.getNestedView(view.elementOffset + this.originator.getBoundElementIndex()); + if (isPresent(nestedView)) this._visitView(nestedView, aggregator); + } else { + this._visit(this.originator, aggregator); + } + this.list.reset(aggregator); + }; + private _visit(inj: ElementInjector, aggregator: any[]): void { + var view = inj.getView(); + var startIdx = view.elementOffset + inj._proto.index; + for (var i = startIdx; i < view.elementOffset + view.ownBindersCount; i++) { + var curInj = view.elementInjectors[i]; + if (isBlank(curInj)) continue; + // The first injector after inj, that is outside the subtree rooted at + // inj has to have a null parent or a parent that is an ancestor of inj. + if (i > startIdx && (isBlank(curInj) || isBlank(curInj.parent) || + view.elementOffset + curInj.parent._proto.index < startIdx)) { + break; + } + + if (!this.query.descendants && + !(curInj.parent == this.originator || curInj == this.originator)) + continue; + + // We visit the view container(VC) views right after the injector that contains + // the VC. Theoretically, that might not be the right order if there are + // child injectors of said injector. Not clear whether if such case can + // even be constructed with the current apis. + this._visitInjector(curInj, aggregator); + var vc = view.viewContainers[i]; + if (isPresent(vc)) this._visitViewContainer(vc, aggregator); + } + } + + private _visitInjector(inj: ElementInjector, aggregator: any[]) { if (this.query.isVarBindingQuery) { this._aggregateVariableBindings(inj, aggregator); } else { this._aggregateDirective(inj, aggregator); } + } - var child = inj._head; - while (isPresent(child)) { - this.visit(child, aggregator); - child = child._next; + private _visitViewContainer(vc: viewModule.AppViewContainer, aggregator: any[]) { + for (var j = 0; j < vc.views.length; j++) { + this._visitView(vc.views[j], aggregator); + } + } + + private _visitView(view: viewModule.AppView, aggregator: any[]) { + for (var i = view.elementOffset; i < view.elementOffset + view.ownBindersCount; i++) { + var inj = view.elementInjectors[i]; + if (isBlank(inj)) continue; + + this._visitInjector(inj, aggregator); + + var vc = view.viewContainers[i]; + if (isPresent(vc)) this._visitViewContainer(vc, aggregator); } } @@ -1171,5 +1019,6 @@ export class QueryRef { reset(): void { this.list.reset([]); this.list.removeAllCallbacks(); + this.dirty = true; } } diff --git a/modules/angular2/src/core/compiler/view.ts b/modules/angular2/src/core/compiler/view.ts index d50cf54591..e483edf83f 100644 --- a/modules/angular2/src/core/compiler/view.ts +++ b/modules/angular2/src/core/compiler/view.ts @@ -211,7 +211,11 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher { } notifyAfterViewChecked(): void { - // required for query + var eiCount = this.proto.elementBinders.length; + var ei = this.elementInjectors; + for (var i = eiCount - 1; i >= 0; i--) { + if (isPresent(ei[i + this.elementOffset])) ei[i + this.elementOffset].afterViewChecked(); + } } getDirectiveFor(directive: DirectiveIndex): any { @@ -289,6 +293,8 @@ export class AppView implements ChangeDispatcher, RenderEventDispatcher { throw new EventEvaluationError(eventName, e, e.stack, context); } } + + get ownBindersCount(): number { return this.proto.elementBinders.length; } } function _localsToStringMap(locals: Locals): StringMap { diff --git a/modules/angular2/src/core/compiler/view_manager_utils.ts b/modules/angular2/src/core/compiler/view_manager_utils.ts index d9f9358bd7..cc41078ca9 100644 --- a/modules/angular2/src/core/compiler/view_manager_utils.ts +++ b/modules/angular2/src/core/compiler/view_manager_utils.ts @@ -122,40 +122,30 @@ export class AppViewManagerUtils { ListWrapper.insert(viewContainer.views, atIndex, view); var elementInjector = contextView.elementInjectors[contextBoundElementIndex]; - var sibling; - if (atIndex == 0) { - sibling = elementInjector; - } else { - sibling = ListWrapper.last(viewContainer.views[atIndex - 1].rootElementInjectors); - } for (var i = view.rootElementInjectors.length - 1; i >= 0; i--) { if (isPresent(elementInjector.parent)) { - view.rootElementInjectors[i].linkAfter(elementInjector.parent, sibling); - } else { - contextView.rootElementInjectors.push(view.rootElementInjectors[i]); + view.rootElementInjectors[i].link(elementInjector.parent); } } + elementInjector.traverseAndSetQueriesAsDirty(); } detachViewInContainer(parentView: viewModule.AppView, boundElementIndex: number, atIndex: number) { var viewContainer = parentView.viewContainers[boundElementIndex]; var view = viewContainer.views[atIndex]; + + parentView.elementInjectors[boundElementIndex].traverseAndSetQueriesAsDirty(); + view.changeDetector.remove(); ListWrapper.removeAt(viewContainer.views, atIndex); for (var i = 0; i < view.rootElementInjectors.length; ++i) { var inj = view.rootElementInjectors[i]; - if (isPresent(inj.parent)) { - inj.unlink(); - } else { - var removeIdx = ListWrapper.indexOf(parentView.rootElementInjectors, inj); - if (removeIdx >= 0) { - ListWrapper.removeAt(parentView.rootElementInjectors, removeIdx); - } - } + inj.unlink(); } } + hydrateViewInContainer(parentView: viewModule.AppView, boundElementIndex: number, contextView: viewModule.AppView, contextBoundElementIndex: number, atIndex: number, imperativelyCreatedBindings: ResolvedBinding[]) { diff --git a/modules/angular2/src/core/metadata.dart b/modules/angular2/src/core/metadata.dart index 6e8f248228..2de191d85a 100644 --- a/modules/angular2/src/core/metadata.dart +++ b/modules/angular2/src/core/metadata.dart @@ -93,8 +93,8 @@ class Query extends QueryMetadata { * See: [ViewQueryMetadata] for docs. */ class ViewQuery extends ViewQueryMetadata { - const ViewQuery(dynamic /*Type | string*/ selector, {bool descendants: false}) - : super(selector, descendants: descendants); + const ViewQuery(dynamic /*Type | string*/ selector) + : super(selector, descendants: true); } /** @@ -111,4 +111,4 @@ class Property extends PropertyMetadata { class Event extends EventMetadata { const Event([String bindingPropertyName]) : super(bindingPropertyName); -} \ No newline at end of file +} diff --git a/modules/angular2/test/core/compiler/element_injector_spec.ts b/modules/angular2/test/core/compiler/element_injector_spec.ts index 43ad44cf44..95428c839f 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.ts +++ b/modules/angular2/test/core/compiler/element_injector_spec.ts @@ -44,12 +44,24 @@ import {TemplateRef} from 'angular2/src/core/compiler/template_ref'; import {ElementRef} from 'angular2/src/core/compiler/element_ref'; import {DynamicChangeDetector, ChangeDetectorRef, Parser, Lexer} from 'angular2/src/core/change_detection/change_detection'; import {QueryList} from 'angular2/src/core/compiler/query_list'; +import {AppView, AppViewContainer} from "angular2/src/core/compiler/view"; -function createDummyView(detector = null) { +function createDummyView(detector = null): AppView { var res = new SpyView(); res.prop("changeDetector", detector); res.prop("elementOffset", 0); - return res; + res.prop("elementInjectors", []); + res.prop("viewContainers", []); + res.prop("ownBindersCount", 0); + return res; +} + +function addInj(view, inj) { + var injs: ElementInjector[] = view.elementInjectors; + injs.push(inj); + var containers: AppViewContainer[] = view.viewContainers; + containers.push(null); + view.prop("ownBindersCount", view.ownBindersCount + 1); } @Injectable() @@ -63,7 +75,7 @@ class SomeOtherDirective {} var _constructionCount = 0; @Injectable() class CountingDirective { - count; + count: number; constructor() { this.count = _constructionCount; _constructionCount += 1; @@ -213,17 +225,8 @@ class DirectiveWithDestroy { onDestroy() { this.onDestroyCounter++; } } -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, createDummyView(), new SpyElementRef(), null); + var defaultPreBuiltObjects = new PreBuiltObjects(null, createDummyView(), new SpyElementRef(), null); // An injector with more than 10 bindings will switch to the dynamic strategy var dynamicBindings = []; @@ -241,15 +244,6 @@ export function main() { return ProtoElementInjector.create(parent, index, directiveBinding, hasShadowRoot, distance, dirVariableBindings); } - function humanize(tree: TreeNode, names: any[][]) { - var lookupName = (item) => - ListWrapper.last(ListWrapper.find(names, (pair) => pair[0] === item)); - - if (tree.children.length == 0) return lookupName(tree); - var children = tree.children.map(m => humanize(m, names)); - return [lookupName(tree), children]; - } - function injector(bindings, imperativelyCreatedInjector = null, isComponent: boolean = false, preBuiltObjects = null, attributes = null, dirVariableBindings = null) { var proto = createPei(null, 0, bindings, 0, isComponent, dirVariableBindings); @@ -290,74 +284,19 @@ export function main() { } describe('TreeNodes', () => { - var root, firstParent, lastParent, node; + var root, child; - /* - 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'); + root = new TreeNode(null); + child = new TreeNode(root); }); - // 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'); + it('should support removing and adding the parent', () => { + expect(child.parent).toEqual(root); + child.remove(); + expect(child.parent).toEqual(null); + root.addChild(child); + expect(child.parent).toEqual(root); }); }); @@ -493,8 +432,9 @@ export function main() { var c1 = protoChild1.instantiate(p); var c2 = protoChild2.instantiate(p); - expect(humanize(p, [[p, 'parent'], [c1, 'child1'], [c2, 'child2']])) - .toEqual(["parent", ["child1", "child2"]]); + expect(c1.parent).toEqual(p); + expect(c2.parent).toEqual(p); + expect(isBlank(p.parent)).toBeTruthy(); }); describe("direct parent", () => { @@ -906,38 +846,6 @@ export function main() { extraBindings)); 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.afterContentChecked(); - })); - - 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.afterContentChecked(); // this does not notify the query - - query.add(new CountingDirective()); - child.parent.afterContentChecked(); - })); }); describe('static attributes', () => { @@ -987,7 +895,7 @@ export function main() { it("should inject ChangeDetectorRef of the containing component into directives", () => { var cd = new DynamicChangeDetector(null, null, 0, [], [], null, [], [], [], null); - var view = createDummyView(cd); + var view = createDummyView(cd); var binding = DirectiveBinding.createFromType(DirectiveNeedsChangeDetectorRef, new DirectiveMetadata()); var inj = injector(ListWrapper.concat([binding], extraBindings), null, false, new PreBuiltObjects(null, view, new SpyElementRef(), null)); @@ -1022,11 +930,17 @@ export function main() { }); describe('queries', () => { - var preBuildObjects = defaultPreBuiltObjects; - beforeEach(() => { _constructionCount = 0; }); + var dummyView; + var preBuildObjects; - function expectDirectives(query, type, expectedIndex) { + beforeEach(() => { _constructionCount = 0; + dummyView = createDummyView(); + preBuildObjects = new PreBuiltObjects(null, dummyView, new SpyElementRef(), null); + }); + + function expectDirectives(query: QueryList, type, expectedIndex) { var currentCount = 0; + expect(query.length).toEqual(expectedIndex.length); iterateListLike(query, (i) => { expect(i).toBeAnInstanceOf(type); expect(i.count).toBe(expectedIndex[currentCount]); @@ -1047,51 +961,25 @@ export function main() { ], extraBindings), null, false, preBuildObjects); + addInj(dummyView, inj); + inj.afterContentChecked(); + expectDirectives(inj.get(NeedsQuery).query, CountingDirective, [0]); - }) + }); it('should contain PreBuiltObjects on the same injector', () => { - var preBuiltObjects = new PreBuiltObjects(null, null, null, new TemplateRef(new SpyElementRef())); + var preBuiltObjects = new PreBuiltObjects(null, dummyView, null, new TemplateRef(new SpyElementRef())); var inj = injector(ListWrapper.concat([ NeedsTemplateRefQuery ], extraBindings), null, false, preBuiltObjects); + addInj(dummyView, inj); + + inj.afterContentChecked(); expect(inj.get(NeedsTemplateRefQuery).query.first).toBe(preBuiltObjects.templateRef); }); - it('should contain multiple directives from the same injector', () => { - var inj = injector(ListWrapper.concat([ - NeedsQuery, - CountingDirective, - FancyCountingDirective, - bind(CountingDirective).toAlias(FancyCountingDirective) - ], extraBindings), null, - false, preBuildObjects); - - expect(inj.get(NeedsQuery).query.length).toEqual(2); - expect(inj.get(NeedsQuery).query.first).toBeAnInstanceOf(CountingDirective); - expect(inj.get(NeedsQuery).query.last).toBeAnInstanceOf(FancyCountingDirective); - }) - - it('should contain multiple directives from the same injector after linking', () => { - var inj = parentChildInjectors([], ListWrapper.concat([ - NeedsQuery, - CountingDirective, - FancyCountingDirective, - bind(CountingDirective).toAlias(FancyCountingDirective) - ], extraBindings)); - - var parent = inj.parent; - - inj.unlink(); - inj.link(parent); - - expect(inj.get(NeedsQuery).query.length).toEqual(2); - expect(inj.get(NeedsQuery).query.first).toBeAnInstanceOf(CountingDirective); - expect(inj.get(NeedsQuery).query.last).toBeAnInstanceOf(FancyCountingDirective); - }) - it('should contain the element when no directives are bound to the var binding', () => { var dirs = [NeedsQueryByVarBindings]; @@ -1102,7 +990,10 @@ export function main() { var inj = injector(dirs.concat(extraBindings), null, false, preBuildObjects, null, dirVariableBindings); - expect(inj.get(NeedsQueryByVarBindings).query.first).toBe(defaultPreBuiltObjects.elementRef); + addInj(dummyView, inj); + inj.afterContentChecked(); + + expect(inj.get(NeedsQueryByVarBindings).query.first).toBe(preBuildObjects.elementRef); }); it('should contain directives on the same injector when querying by variable bindings' + @@ -1117,21 +1008,14 @@ export function main() { var inj = injector(dirs.concat(extraBindings), null, false, preBuildObjects, null, dirVariableBindings); + addInj(dummyView, inj); + inj.afterContentChecked(); + // NeedsQueryByVarBindings queries "one,two", so SimpleDirective should be before NeedsDirective expect(inj.get(NeedsQueryByVarBindings).query.first).toBeAnInstanceOf(SimpleDirective); expect(inj.get(NeedsQueryByVarBindings).query.last).toBeAnInstanceOf(NeedsDirective); }); - // 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 = createPei(null, 0, [NeedsQuery, CountingDirective]); var protoChild = @@ -1142,90 +1026,12 @@ export function main() { parent.hydrate(null, null, preBuildObjects); child.hydrate(null, null, preBuildObjects); + addInj(dummyView, parent); + addInj(dummyView, child); + parent.afterContentChecked(); + expectDirectives(parent.get(NeedsQuery).query, CountingDirective, [0, 1]); }); - - it('should reflect unlinking an injector', () => { - var protoParent = createPei(null, 0, [NeedsQuery, CountingDirective]); - var protoChild = - createPei(protoParent, 1, ListWrapper.concat([CountingDirective], extraBindings)); - - var parent = protoParent.instantiate(null); - var child = protoChild.instantiate(parent); - parent.hydrate(null, null, preBuildObjects); - child.hydrate(null, null, preBuildObjects); - - child.unlink(); - - expectDirectives(parent.get(NeedsQuery).query, CountingDirective, [0]); - }); - - it('should reflect moving an injector as a last child', () => { - var protoParent = createPei(null, 0, [NeedsQuery, CountingDirective]); - var protoChild1 = createPei(protoParent, 1, [CountingDirective]); - var protoChild2 = - createPei(protoParent, 1, ListWrapper.concat([CountingDirective], extraBindings)); - - var parent = protoParent.instantiate(null); - var child1 = protoChild1.instantiate(parent); - var child2 = protoChild2.instantiate(parent); - - parent.hydrate(null, null, preBuildObjects); - child1.hydrate(null, null, preBuildObjects); - child2.hydrate(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 = createPei(null, 0, [NeedsQuery, CountingDirective]); - var protoChild1 = createPei(protoParent, 1, [CountingDirective]); - var protoChild2 = - createPei(protoParent, 1, ListWrapper.concat([CountingDirective], extraBindings)); - - var parent = protoParent.instantiate(null); - var child1 = protoChild1.instantiate(parent); - var child2 = protoChild2.instantiate(parent); - - parent.hydrate(null, null, preBuildObjects); - child1.hydrate(null, null, preBuildObjects); - child2.hydrate(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 = createPei(null, 0, [NeedsQuery]); - var protoParent = createPei(null, 0, [NeedsQuery]); - var protoChild = - createPei(protoParent, 1, ListWrapper.concat([CountingDirective], extraBindings)); - - var grandParent = protoGrandParent.instantiate(null); - var parent = protoParent.instantiate(grandParent); - var child = protoChild.instantiate(parent); - - grandParent.hydrate(null, null, preBuildObjects); - parent.hydrate(null, null, preBuildObjects); - child.hydrate(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, []); - }); }); }); }); diff --git a/modules/angular2/test/core/compiler/query_integration_spec.ts b/modules/angular2/test/core/compiler/query_integration_spec.ts index 255ad25331..74e628749a 100644 --- a/modules/angular2/test/core/compiler/query_integration_spec.ts +++ b/modules/angular2/test/core/compiler/query_integration_spec.ts @@ -382,30 +382,13 @@ export function main() { view.detectChanges(); - expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "2", "3"]); - - async.done(); - }); - })); - - it('should query descendants in the view when the flag is used', - inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { - var template = ''; - - tcb.overrideTemplate(MyComp, template) - .createAsync(MyComp) - .then((view) => { - var q: NeedsViewQueryDesc = view.componentViewChildren[0].getLocal("q"); - - view.detectChanges(); - expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "2", "3", "4"]); async.done(); }); })); - it('should include directive present on the host element', + it('should not include directive present on the host element', inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { var template = ''; @@ -416,7 +399,7 @@ export function main() { view.detectChanges(); - expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "2", "3"]); + expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "2", "3", "4"]); async.done(); }); @@ -437,6 +420,7 @@ export function main() { q.show = true; view.detectChanges(); + expect(q.query.length).toBe(1); expect(q.query.first.text).toEqual("1"); @@ -486,6 +470,29 @@ export function main() { view.detectChanges(); + expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "-3", "2", "4"]); + + async.done(); + }); + })); + + it('should maintain directives in pre-order depth-first DOM order after dynamic insertion', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var template = ''; + + tcb.overrideTemplate(MyComp, template) + .createAsync(MyComp) + .then((view) => { + var q: NeedsViewQueryOrderWithParent = view.componentViewChildren[0].getLocal("q"); + + view.detectChanges(); + + expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "2", "3", "4"]); + + q.list = ["-3", "2"]; + view.detectChanges(); + + expect(q.query.map((d: TextDirective) => d.text)).toEqual(["1", "-3", "2", "4"]); async.done(); @@ -566,9 +573,7 @@ class NeedsQueryByLabel { @Injectable() class NeedsViewQueryByLabel { query: QueryList; - constructor(@ViewQuery("textLabel", {descendants: true}) query: QueryList) { - this.query = query; - } + constructor(@ViewQuery("textLabel") query: QueryList) { this.query = query; } } @Component({selector: 'needs-query-by-var-bindings'}) @@ -595,8 +600,8 @@ class NeedsQueryAndProject { @Component({selector: 'needs-view-query'}) @View({ directives: [TextDirective], - template: '
' + - '
' + template: '
' + + '
' }) @Injectable() class NeedsViewQuery { @@ -604,20 +609,6 @@ class NeedsViewQuery { constructor(@ViewQuery(TextDirective) query: QueryList) { this.query = query; } } -@Component({selector: 'needs-view-query-desc'}) -@View({ - directives: [TextDirective], - template: '
' + - '
' -}) -@Injectable() -class NeedsViewQueryDesc { - query: QueryList; - constructor(@ViewQuery(TextDirective, {descendants: true}) query: QueryList) { - this.query = query; - } -} - @Component({selector: 'needs-view-query-if'}) @View({directives: [NgIf, TextDirective], template: '
'}) @Injectable() @@ -646,10 +637,24 @@ class NeedsViewQueryNestedIf { } } - -// TODO(rado): once https://github.com/angular/angular/issues/3438 is resolved -// duplicate the test without InertDirective. @Component({selector: 'needs-view-query-order'}) +@View({ + directives: [NgFor, TextDirective, InertDirective], + template: '
' + + '
' + + '
' +}) +@Injectable() +class NeedsViewQueryOrder { + query: QueryList; + list: string[]; + constructor(@ViewQuery(TextDirective) query: QueryList) { + this.query = query; + this.list = ['2', '3']; + } +} + +@Component({selector: 'needs-view-query-order-with-p'}) @View({ directives: [NgFor, TextDirective, InertDirective], template: '
' + @@ -657,10 +662,10 @@ class NeedsViewQueryNestedIf { '
' }) @Injectable() -class NeedsViewQueryOrder { +class NeedsViewQueryOrderWithParent { query: QueryList; list: string[]; - constructor(@ViewQuery(TextDirective, {descendants: true}) query: QueryList) { + constructor(@ViewQuery(TextDirective) query: QueryList) { this.query = query; this.list = ['2', '3']; } @@ -687,11 +692,11 @@ class NeedsTpl { NeedsQueryByTwoLabels, NeedsQueryAndProject, NeedsViewQuery, - NeedsViewQueryDesc, NeedsViewQueryIf, NeedsViewQueryNestedIf, NeedsViewQueryOrder, NeedsViewQueryByLabel, + NeedsViewQueryOrderWithParent, NeedsTpl, TextDirective, InertDirective, diff --git a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts index 40c2c0534c..debf75aded 100644 --- a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts @@ -131,24 +131,23 @@ export function main() { var binders = []; for (var i = 0; i < numInj; i++) { binders.push(createEmptyElBinder(i > 0 ? binders[i - 1] : null)) - }; + } var contextPv = createHostPv(binders); contextView = createViewWithChildren(contextPv); } - it('should link the views rootElementInjectors at the given context', () => { + it('should not modify the rootElementInjectors at the given context view', () => { createViews(); utils.attachViewInContainer(parentView, 0, contextView, 0, 0, childView); - expect(contextView.rootElementInjectors.length).toEqual(2); + expect(contextView.rootElementInjectors.length).toEqual(1); }); it('should link the views rootElementInjectors after the elementInjector at the given context', () => { createViews(2); utils.attachViewInContainer(parentView, 0, contextView, 1, 0, childView); - expect(childView.rootElementInjectors[0].spy('linkAfter')) - .toHaveBeenCalledWith(contextView.elementInjectors[0], - contextView.elementInjectors[1]); + expect(childView.rootElementInjectors[0].spy('link')) + .toHaveBeenCalledWith(contextView.elementInjectors[0]); }); });