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:
parent
61cb99ea42
commit
e9f70293ac
|
@ -54,3 +54,15 @@ export class Attribute extends DependencyAnnotation {
|
||||||
return this;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,13 +3,14 @@ import {Math} from 'angular2/src/facade/math';
|
||||||
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||||
import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'angular2/di';
|
import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'angular2/di';
|
||||||
import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility';
|
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 * as viewModule from 'angular2/src/core/compiler/view';
|
||||||
import {ViewContainer} from 'angular2/src/core/compiler/view_container';
|
import {ViewContainer} from 'angular2/src/core/compiler/view_container';
|
||||||
import {NgElement} from 'angular2/src/core/compiler/ng_element';
|
import {NgElement} from 'angular2/src/core/compiler/ng_element';
|
||||||
import {Directive, onChange, onDestroy, onAllChangesDone} from 'angular2/src/core/annotations/annotations';
|
import {Directive, onChange, onDestroy, onAllChangesDone} from 'angular2/src/core/annotations/annotations';
|
||||||
import {BindingPropagationConfig} from 'angular2/change_detection';
|
import {BindingPropagationConfig} from 'angular2/change_detection';
|
||||||
import * as pclModule from 'angular2/src/core/compiler/private_component_location';
|
import * as pclModule from 'angular2/src/core/compiler/private_component_location';
|
||||||
|
import {QueryList} from './query_list';
|
||||||
|
|
||||||
var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10;
|
var _MAX_DIRECTIVE_CONSTRUCTION_COUNTER = 10;
|
||||||
|
|
||||||
|
@ -41,39 +42,123 @@ class StaticKeys {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TreeNode {
|
export class TreeNode {
|
||||||
_parent:TreeNode;
|
_parent:TreeNode;
|
||||||
_head:TreeNode;
|
_head:TreeNode;
|
||||||
_tail:TreeNode;
|
_tail:TreeNode;
|
||||||
_next:TreeNode;
|
_next:TreeNode;
|
||||||
constructor(parent:TreeNode) {
|
constructor(parent:TreeNode) {
|
||||||
this._parent = parent;
|
|
||||||
this._head = null;
|
this._head = null;
|
||||||
this._tail = null;
|
this._tail = null;
|
||||||
this._next = 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.
|
* 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)) {
|
if (isPresent(this._tail)) {
|
||||||
this._tail._next = child;
|
this._tail._next = child;
|
||||||
this._tail = child;
|
this._tail = child;
|
||||||
} else {
|
} else {
|
||||||
this._tail = this._head = child;
|
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() {
|
get parent() {
|
||||||
return this._parent;
|
return this._parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
set parent(node:TreeNode) {
|
// TODO(rado): replace with a function call, does too much work for a getter.
|
||||||
this._parent = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
get children() {
|
get children() {
|
||||||
var res = [];
|
var res = [];
|
||||||
var child = this._head;
|
var child = this._head;
|
||||||
|
@ -90,14 +175,28 @@ export class DirectiveDependency extends Dependency {
|
||||||
eventEmitterName:string;
|
eventEmitterName:string;
|
||||||
propSetterName:string;
|
propSetterName:string;
|
||||||
attributeName:string;
|
attributeName:string;
|
||||||
|
queryDirective;
|
||||||
|
|
||||||
constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean,
|
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);
|
super(key, asPromise, lazy, optional, properties);
|
||||||
this.depth = depth;
|
this.depth = depth;
|
||||||
this.eventEmitterName = eventEmitterName;
|
this.eventEmitterName = eventEmitterName;
|
||||||
this.propSetterName = propSetterName;
|
this.propSetterName = propSetterName;
|
||||||
this.attributeName = attributeName;
|
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 {
|
static createFrom(d:Dependency):Dependency {
|
||||||
|
@ -106,6 +205,7 @@ export class DirectiveDependency extends Dependency {
|
||||||
var propName = null;
|
var propName = null;
|
||||||
var attributeName = null;
|
var attributeName = null;
|
||||||
var properties = d.properties;
|
var properties = d.properties;
|
||||||
|
var queryDirective = null;
|
||||||
|
|
||||||
for (var i = 0; i < properties.length; i++) {
|
for (var i = 0; i < properties.length; i++) {
|
||||||
var property = properties[i];
|
var property = properties[i];
|
||||||
|
@ -119,11 +219,13 @@ export class DirectiveDependency extends Dependency {
|
||||||
propName = property.propName;
|
propName = property.propName;
|
||||||
} else if (property instanceof Attribute) {
|
} else if (property instanceof Attribute) {
|
||||||
attributeName = property.attributeName;
|
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,
|
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;
|
_privateComponent;
|
||||||
_privateComponentBinding:DirectiveBinding;
|
_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) {
|
constructor(proto:ProtoElementInjector, parent:ElementInjector) {
|
||||||
super(parent);
|
super(parent);
|
||||||
this._proto = proto;
|
this._proto = proto;
|
||||||
|
@ -358,6 +465,9 @@ export class ElementInjector extends TreeNode {
|
||||||
this._obj8 = null;
|
this._obj8 = null;
|
||||||
this._obj9 = null;
|
this._obj9 = null;
|
||||||
this._constructionCounter = 0;
|
this._constructionCounter = 0;
|
||||||
|
|
||||||
|
this._inheritQueries(parent);
|
||||||
|
this._buildQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDirectives() {
|
clearDirectives() {
|
||||||
|
@ -523,6 +633,8 @@ export class ElementInjector extends TreeNode {
|
||||||
default: throw `Directive ${binding.key.token} can only have up to 10 dependencies.`;
|
default: throw `Directive ${binding.key.token} can only have up to 10 dependencies.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._addToQueries(obj, binding.key.token);
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -534,10 +646,11 @@ export class ElementInjector extends TreeNode {
|
||||||
if (isPresent(dep.eventEmitterName)) return this._buildEventEmitter(dep);
|
if (isPresent(dep.eventEmitterName)) return this._buildEventEmitter(dep);
|
||||||
if (isPresent(dep.propSetterName)) return this._buildPropSetter(dep);
|
if (isPresent(dep.propSetterName)) return this._buildPropSetter(dep);
|
||||||
if (isPresent(dep.attributeName)) return this._buildAttribute(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);
|
return this._getByKey(dep.key, dep.depth, dep.optional, requestor);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildEventEmitter(dep) {
|
_buildEventEmitter(dep: DirectiveDependency) {
|
||||||
var view = this._getPreBuiltObjectByKeyId(StaticKeys.instance().viewId);
|
var view = this._getPreBuiltObjectByKeyId(StaticKeys.instance().viewId);
|
||||||
return (event) => {
|
return (event) => {
|
||||||
view.triggerEventHandlers(dep.eventEmitterName, event, this._proto.index);
|
view.triggerEventHandlers(dep.eventEmitterName, event, this._proto.index);
|
||||||
|
@ -562,6 +675,120 @@ export class ElementInjector extends TreeNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildQueriesForDeps(deps: List<DirectiveDependency>) {
|
||||||
|
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.
|
* It is fairly easy to annotate keys with metadata.
|
||||||
* For example, key.metadata = 'directive'.
|
* For example, key.metadata = 'directive'.
|
||||||
|
@ -700,3 +927,45 @@ class OutOfBoundsAccess extends Error {
|
||||||
return this.message;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<Directive> {
|
||||||
|
List<Directive> _results;
|
||||||
|
List _callbacks;
|
||||||
|
bool _dirty;
|
||||||
|
|
||||||
|
QueryList(): _results = [], _callbacks = [], _dirty = false;
|
||||||
|
|
||||||
|
Iterator<Directive> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Directive>;
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,11 @@ export class ViewContainer {
|
||||||
return this._views.length;
|
return this._views.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_siblingInjectorToLinkAfter(index: number) {
|
||||||
|
if (index == 0) return null;
|
||||||
|
return ListWrapper.last(this._views[index - 1].rootElementInjectors)
|
||||||
|
}
|
||||||
|
|
||||||
hydrated() {
|
hydrated() {
|
||||||
return isPresent(this.appInjector);
|
return isPresent(this.appInjector);
|
||||||
}
|
}
|
||||||
|
@ -106,7 +111,7 @@ export class ViewContainer {
|
||||||
if (atIndex == -1) atIndex = this._views.length;
|
if (atIndex == -1) atIndex = this._views.length;
|
||||||
ListWrapper.insert(this._views, atIndex, view);
|
ListWrapper.insert(this._views, atIndex, view);
|
||||||
this.parentView.changeDetector.addChild(view.changeDetector);
|
this.parentView.changeDetector.addChild(view.changeDetector);
|
||||||
this._linkElementInjectors(view);
|
this._linkElementInjectors(this._siblingInjectorToLinkAfter(atIndex), view);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
@ -133,15 +138,19 @@ export class ViewContainer {
|
||||||
return detachedView;
|
return detachedView;
|
||||||
}
|
}
|
||||||
|
|
||||||
_linkElementInjectors(view) {
|
contentTagContainers() {
|
||||||
for (var i = 0; i < view.rootElementInjectors.length; ++i) {
|
return this._views;
|
||||||
view.rootElementInjectors[i].parent = this.elementInjector;
|
}
|
||||||
|
|
||||||
|
_linkElementInjectors(sibling, view) {
|
||||||
|
for (var i = view.rootElementInjectors.length - 1; i >= 0; i--) {
|
||||||
|
view.rootElementInjectors[i].linkAfter(this.elementInjector, sibling);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_unlinkElementInjectors(view) {
|
_unlinkElementInjectors(view) {
|
||||||
for (var i = 0; i < view.rootElementInjectors.length; ++i) {
|
for (var i = 0; i < view.rootElementInjectors.length; ++i) {
|
||||||
view.rootElementInjectors[i].parent = null;
|
view.rootElementInjectors[i].unlink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,6 +122,7 @@ export class ViewContainer {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static moveViewNodesAfterSibling(sibling, view) {
|
static moveViewNodesAfterSibling(sibling, view) {
|
||||||
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
|
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
|
||||||
DOM.insertAfter(sibling, view.rootNodes[i]);
|
DOM.insertAfter(sibling, view.rootNodes[i]);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {describe, ddescribe, it, iit, xit, xdescribe, expect, beforeEach, SpyObject, proxy, el} from 'angular2/test_lib';
|
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 {isBlank, isPresent, IMPLEMENTS} from 'angular2/src/facade/lang';
|
||||||
import {ListWrapper, MapWrapper, List, StringMapWrapper} from 'angular2/src/facade/collection';
|
import {ListWrapper, MapWrapper, List, StringMapWrapper, iterateListLike} from 'angular2/src/facade/collection';
|
||||||
import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding} from 'angular2/src/core/compiler/element_injector';
|
import {ProtoElementInjector, PreBuiltObjects, DirectiveBinding, TreeNode} from 'angular2/src/core/compiler/element_injector';
|
||||||
import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility';
|
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 {onDestroy} from 'angular2/src/core/annotations/annotations';
|
||||||
import {Optional, Injector, Inject, bind} from 'angular2/di';
|
import {Optional, Injector, Inject, bind} from 'angular2/di';
|
||||||
import {ProtoView, View} from 'angular2/src/core/compiler/view';
|
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 {BindingPropagationConfig, Parser, Lexer} from 'angular2/change_detection';
|
||||||
|
|
||||||
import {ViewRef, Renderer} from 'angular2/src/render/api';
|
import {ViewRef, Renderer} from 'angular2/src/render/api';
|
||||||
|
import {QueryList} from 'angular2/src/core/compiler/query_list';
|
||||||
|
|
||||||
class DummyDirective extends Directive {
|
class DummyDirective extends Directive {
|
||||||
constructor({lifecycle} = {}) { super({lifecycle: lifecycle}); }
|
constructor({lifecycle} = {}) { super({lifecycle: lifecycle}); }
|
||||||
|
@ -26,10 +27,24 @@ class DummyView extends SpyObject {noSuchMethod(m){super.noSuchMethod(m)}}
|
||||||
class SimpleDirective {
|
class SimpleDirective {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SomeOtherDirective {
|
class SomeOtherDirective {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _constructionCount = 0;
|
||||||
|
class CountingDirective {
|
||||||
|
count;
|
||||||
|
constructor() {
|
||||||
|
this.count = _constructionCount;
|
||||||
|
_constructionCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FancyCountingDirective extends CountingDirective {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NeedsDirective {
|
class NeedsDirective {
|
||||||
dependency:SimpleDirective;
|
dependency:SimpleDirective;
|
||||||
constructor(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 {
|
class A_Needs_B {
|
||||||
constructor(dep){}
|
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() {
|
export function main() {
|
||||||
var defaultPreBuiltObjects = new PreBuiltObjects(null, null, null, null);
|
var defaultPreBuiltObjects = new PreBuiltObjects(null, null, null, null);
|
||||||
var appInjector = new Injector([]);
|
var appInjector = new Injector([]);
|
||||||
|
@ -235,6 +268,81 @@ export function main() {
|
||||||
return shadow;
|
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("ProtoElementInjector", () => {
|
||||||
describe("direct parent", () => {
|
describe("direct parent", () => {
|
||||||
it("should return parent proto injector when distance is 1", () => {
|
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", () => {
|
it("should not instantiate directives that depend on other directives in the containing component's ElementInjector", () => {
|
||||||
expect( () => {
|
expect(() => {
|
||||||
hostShadowInjectors([SomeOtherDirective, SimpleDirective], [NeedsDirective]);
|
hostShadowInjectors([SomeOtherDirective, SimpleDirective], [NeedsDirective]);
|
||||||
}).toThrowError('No provider for SimpleDirective! (NeedsDirective -> SimpleDirective)')
|
}).toThrowError('No provider for SimpleDirective! (NeedsDirective -> SimpleDirective)')
|
||||||
});
|
});
|
||||||
|
@ -394,7 +502,7 @@ export function main() {
|
||||||
var shadowAppInjector = new Injector([
|
var shadowAppInjector = new Injector([
|
||||||
bind("service").toValue("service")
|
bind("service").toValue("service")
|
||||||
]);
|
]);
|
||||||
expect( () => {
|
expect(() => {
|
||||||
injector([SomeOtherDirective, NeedsService], null, shadowAppInjector);
|
injector([SomeOtherDirective, NeedsService], null, shadowAppInjector);
|
||||||
}).toThrowError('No provider for service! (NeedsService -> service)');
|
}).toThrowError('No provider for service! (NeedsService -> service)');
|
||||||
});
|
});
|
||||||
|
@ -434,7 +542,7 @@ export function main() {
|
||||||
|
|
||||||
it("should throw when no SimpleDirective found", function () {
|
it("should throw when no SimpleDirective found", function () {
|
||||||
expect(() => injector([NeedDirectiveFromParent])).
|
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 () {
|
it("should inject null when no directive found", function () {
|
||||||
|
@ -470,7 +578,7 @@ export function main() {
|
||||||
DirectiveBinding.createFromBinding(bBneedsA, null)
|
DirectiveBinding.createFromBinding(bBneedsA, null)
|
||||||
]);
|
]);
|
||||||
}).toThrowError('Cannot instantiate cyclic dependency! ' +
|
}).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() {
|
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]);
|
ListWrapper.push(this.log, [viewRef, elementIndex, propertyName, value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue