From b0e2ebda708cf25287a211a30ef63d84cdb92a7f Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 15 Jun 2015 15:18:11 -0700 Subject: [PATCH] feat(query): added support for querying by var bindings --- .../angular2/src/core/annotations_impl/di.ts | 9 +- .../src/core/compiler/element_binder.ts | 1 - .../src/core/compiler/element_injector.ts | 75 +++- .../src/core/compiler/proto_view_factory.ts | 20 +- modules/angular2/src/core/compiler/view.ts | 6 +- .../src/core/compiler/view_manager_utils.ts | 24 +- .../test/core/compiler/compiler_spec.ts | 4 +- .../core/compiler/element_injector_spec.ts | 48 ++- .../core/compiler/query_integration_spec.ts | 395 ++++++++++++------ .../test/core/compiler/view_manager_spec.ts | 4 +- .../core/compiler/view_manager_utils_spec.ts | 4 +- .../element_injector_benchmark.ts | 2 +- 12 files changed, 407 insertions(+), 185 deletions(-) diff --git a/modules/angular2/src/core/annotations_impl/di.ts b/modules/angular2/src/core/annotations_impl/di.ts index e10be1c748..d6733e8b13 100644 --- a/modules/angular2/src/core/annotations_impl/di.ts +++ b/modules/angular2/src/core/annotations_impl/di.ts @@ -1,4 +1,4 @@ -import {CONST, Type, stringify, isPresent} from 'angular2/src/facade/lang'; +import {CONST, Type, stringify, isPresent, StringWrapper, isString} from 'angular2/src/facade/lang'; import {DependencyAnnotation} from 'angular2/src/di/annotations_impl'; import {resolveForwardRef} from 'angular2/di'; @@ -55,12 +55,17 @@ export class Attribute extends DependencyAnnotation { @CONST() export class Query extends DependencyAnnotation { descendants: boolean; - constructor(private _selector:Type, {descendants = false}: {descendants?: boolean} = {}) { + constructor(private _selector: Type | string, + {descendants = false}: {descendants?: boolean} = {}) { super(); this.descendants = descendants; } get selector() { return resolveForwardRef(this._selector); } + get isVarBindingQuery(): boolean { return isString(this.selector); } + + get varBindings(): List { return StringWrapper.split(this.selector, new RegExp(",")); } + toString() { return `@Query(${stringify(this.selector)})`; } } diff --git a/modules/angular2/src/core/compiler/element_binder.ts b/modules/angular2/src/core/compiler/element_binder.ts index 74e3724cff..9bd1d1edbb 100644 --- a/modules/angular2/src/core/compiler/element_binder.ts +++ b/modules/angular2/src/core/compiler/element_binder.ts @@ -13,7 +13,6 @@ export class ElementBinder { constructor(public index: int, public parent: ElementBinder, public distanceToParent: int, public protoElementInjector: eiModule.ProtoElementInjector, - public directiveVariableBindings: Map, public componentDirective: DirectiveBinding) { if (isBlank(index)) { throw new BaseException('null index not allowed.'); diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index 85117ff4c3..c41b4b890c 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -393,7 +393,8 @@ export class ProtoElementInjector { _strategy: _ProtoElementInjectorStrategy; static create(parent: ProtoElementInjector, index: number, bindings: List, - firstBindingIsComponent: boolean, distanceToParent: number) { + firstBindingIsComponent: boolean, distanceToParent: number, + directiveVariableBindings: Map) { var bd = []; ProtoElementInjector._createDirectiveBindingData(bindings, bd, firstBindingIsComponent); @@ -401,7 +402,8 @@ export class ProtoElementInjector { ProtoElementInjector._createViewInjectorBindingData(bindings, bd); } ProtoElementInjector._createHostInjectorBindingData(bindings, bd, firstBindingIsComponent); - return new ProtoElementInjector(parent, index, bd, distanceToParent, firstBindingIsComponent); + return new ProtoElementInjector(parent, index, bd, distanceToParent, firstBindingIsComponent, + directiveVariableBindings); } private static _createDirectiveBindingData(dirBindings: List, @@ -450,7 +452,8 @@ export class ProtoElementInjector { } constructor(public parent: ProtoElementInjector, public index: int, bd: List, - public distanceToParent: number, public _firstBindingIsComponent: boolean) { + public distanceToParent: number, public _firstBindingIsComponent: boolean, + public directiveVariableBindings: Map) { var length = bd.length; this.eventEmitterAccessors = ListWrapper.createFixedSize(length); this.hostActionAccessors = ListWrapper.createFixedSize(length); @@ -693,12 +696,15 @@ export class ElementInjector extends TreeNode { } onAllChangesDone(): void { - if (isPresent(this._query0) && this._query0.originator === this) + if (isPresent(this._query0) && this._query0.originator === this) { this._query0.list.fireCallbacks(); - if (isPresent(this._query1) && this._query1.originator === this) + } + if (isPresent(this._query1) && this._query1.originator === this) { this._query1.list.fireCallbacks(); - if (isPresent(this._query2) && this._query2.originator === this) + } + if (isPresent(this._query2) && this._query2.originator === this) { this._query2.list.fireCallbacks(); + } } hydrate(injector: Injector, host: ElementInjector, preBuiltObjects: PreBuiltObjects): void { @@ -716,9 +722,22 @@ export class ElementInjector extends TreeNode { this._checkShadowDomAppInjector(this._shadowDomAppInjector); this._strategy.hydrate(); + + this._addVarBindingsToQueries(); + this.hydrated = true; } + hasVariableBinding(name: string): boolean { + var vb = this._proto.directiveVariableBindings; + return isPresent(vb) && MapWrapper.contains(vb, name); + } + + getVariableBinding(name: string): any { + var index = MapWrapper.get(this._proto.directiveVariableBindings, name); + return isPresent(index) ? this.getDirectiveAtIndex(index) : this.getElementRef(); + } + private _createShadowDomAppInjector(componentDirective: DirectiveBinding, appInjector: Injector): Injector { if (!ListWrapper.isEmpty(componentDirective.resolvedAppInjectables)) { @@ -752,6 +771,10 @@ export class ElementInjector extends TreeNode { return this._proto.hostActionAccessors; } + getDirectiveVariableBindings(): Map { + return this._proto.directiveVariableBindings; + } + getComponent(): any { return this._strategy.getComponent(); } getElementRef(): ElementRef { @@ -884,6 +907,23 @@ export class ElementInjector extends TreeNode { } } + private _addVarBindingsToQueries(): void { + this._addVarBindingsToQuery(this._query0); + this._addVarBindingsToQuery(this._query1); + this._addVarBindingsToQuery(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 _createQueryRef(query: Query): void { var queryList = new QueryList(); if (isBlank(this._query0)) { @@ -1454,13 +1494,32 @@ class QueryRef { visit(inj: ElementInjector, aggregator: any[]): void { if (isBlank(inj) || !inj._hasQuery(this)) return; - if (inj.hasDirective(this.query.selector)) { - aggregator.push(inj.get(this.query.selector)); + + 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 _aggregateVariableBindings(inj: ElementInjector, aggregator: List): void { + var vb = this.query.varBindings; + for (var i = 0; i < vb.length; ++i) { + if (inj.hasVariableBinding(vb[i])) { + aggregator.push(inj.getVariableBinding(vb[i])); + } + } + } + + private _aggregateDirective(inj: ElementInjector, aggregator: List): void { + if (inj.hasDirective(this.query.selector)) { + aggregator.push(inj.get(this.query.selector)); + } + } } diff --git a/modules/angular2/src/core/compiler/proto_view_factory.ts b/modules/angular2/src/core/compiler/proto_view_factory.ts index 93de3ca605..63d6810f3b 100644 --- a/modules/angular2/src/core/compiler/proto_view_factory.ts +++ b/modules/angular2/src/core/compiler/proto_view_factory.ts @@ -336,9 +336,13 @@ function _createProtoElementInjector(binderIndex, parentPeiWithDistance, renderE // so that, when hydrating, $implicit can be set to the element. var hasVariables = MapWrapper.size(renderElementBinder.variableBindings) > 0; if (directiveBindings.length > 0 || hasVariables) { - protoElementInjector = ProtoElementInjector.create( - parentPeiWithDistance.protoElementInjector, binderIndex, directiveBindings, - isPresent(componentDirectiveBinding), parentPeiWithDistance.distance); + var directiveVariableBindings = + createDirectiveVariableBindings(renderElementBinder, directiveBindings); + + protoElementInjector = + ProtoElementInjector.create(parentPeiWithDistance.protoElementInjector, binderIndex, + directiveBindings, isPresent(componentDirectiveBinding), + parentPeiWithDistance.distance, directiveVariableBindings); protoElementInjector.attributes = renderElementBinder.readAttributes; } return protoElementInjector; @@ -351,12 +355,8 @@ function _createElementBinder(protoView, boundElementIndex, renderElementBinder, if (renderElementBinder.parentIndex !== -1) { parent = protoView.elementBinders[renderElementBinder.parentIndex]; } - - var directiveVariableBindings = - createDirectiveVariableBindings(renderElementBinder, directiveBindings); - var elBinder = - protoView.bindElement(parent, renderElementBinder.distanceToParent, protoElementInjector, - directiveVariableBindings, componentDirectiveBinding); + var elBinder = protoView.bindElement(parent, renderElementBinder.distanceToParent, + protoElementInjector, componentDirectiveBinding); protoView.bindEvent(renderElementBinder.eventBindings, boundElementIndex, -1); // variables // The view's locals needs to have a full set of variable names at construction time @@ -371,7 +371,7 @@ function _createElementBinder(protoView, boundElementIndex, renderElementBinder, export function createDirectiveVariableBindings( renderElementBinder: renderApi.ElementBinder, - directiveBindings: List): Map { + directiveBindings: List): Map { var directiveVariableBindings = MapWrapper.create(); MapWrapper.forEach(renderElementBinder.variableBindings, (templateName, exportAs) => { var dirIndex = _findDirectiveIndexByExportAs(renderElementBinder, directiveBindings, exportAs); diff --git a/modules/angular2/src/core/compiler/view.ts b/modules/angular2/src/core/compiler/view.ts index 1778119c03..3a4eef8f8d 100644 --- a/modules/angular2/src/core/compiler/view.ts +++ b/modules/angular2/src/core/compiler/view.ts @@ -177,11 +177,9 @@ export class AppProtoView { bindElement(parent: ElementBinder, distanceToParent: int, protoElementInjector: ProtoElementInjector, - directiveVariableBindings: Map, componentDirective: DirectiveBinding = null): ElementBinder { - var elBinder = - new ElementBinder(this.elementBinders.length, parent, distanceToParent, - protoElementInjector, directiveVariableBindings, componentDirective); + var elBinder = new ElementBinder(this.elementBinders.length, parent, distanceToParent, + protoElementInjector, componentDirective); this.elementBinders.push(elBinder); return elBinder; diff --git a/modules/angular2/src/core/compiler/view_manager_utils.ts b/modules/angular2/src/core/compiler/view_manager_utils.ts index cd792940c6..3ec255282a 100644 --- a/modules/angular2/src/core/compiler/view_manager_utils.ts +++ b/modules/angular2/src/core/compiler/view_manager_utils.ts @@ -149,28 +149,30 @@ export class AppViewManagerUtils { var binders = view.proto.elementBinders; for (var i = 0; i < binders.length; ++i) { - var binder = binders[i]; var elementInjector = view.elementInjectors[i]; if (isPresent(elementInjector)) { elementInjector.hydrate(appInjector, hostElementInjector, view.preBuiltObjects[i]); + this._populateViewLocals(view, elementInjector); this._setUpEventEmitters(view, elementInjector, i); this._setUpHostActions(view, elementInjector, i); - - if (isPresent(binder.directiveVariableBindings)) { - MapWrapper.forEach(binder.directiveVariableBindings, (directiveIndex, name) => { - if (isBlank(directiveIndex)) { - view.locals.set(name, elementInjector.getElementRef().domElement); - } else { - view.locals.set(name, elementInjector.getDirectiveAtIndex(directiveIndex)); - } - }); - } } } view.changeDetector.hydrate(view.context, view.locals, view); } + _populateViewLocals(view: viewModule.AppView, elementInjector: eli.ElementInjector): void { + if (isPresent(elementInjector.getDirectiveVariableBindings())) { + MapWrapper.forEach(elementInjector.getDirectiveVariableBindings(), (directiveIndex, name) => { + if (isBlank(directiveIndex)) { + view.locals.set(name, elementInjector.getElementRef().domElement); + } else { + view.locals.set(name, elementInjector.getDirectiveAtIndex(directiveIndex)); + } + }); + } + } + _getOrCreateViewContainer(parentView: viewModule.AppView, boundElementIndex: number) { var viewContainer = parentView.viewContainers[boundElementIndex]; if (isBlank(viewContainer)) { diff --git a/modules/angular2/test/core/compiler/compiler_spec.ts b/modules/angular2/test/core/compiler/compiler_spec.ts index a88ceb4f7f..52d4a05273 100644 --- a/modules/angular2/test/core/compiler/compiler_spec.ts +++ b/modules/angular2/test/core/compiler/compiler_spec.ts @@ -481,11 +481,11 @@ function createProtoView(elementBinders = null) { function createComponentElementBinder(directiveResolver, type) { var binding = createDirectiveBinding(directiveResolver, type); - return new ElementBinder(0, null, 0, null, null, binding); + return new ElementBinder(0, null, 0, null, binding); } function createViewportElementBinder(nestedProtoView) { - var elBinder = new ElementBinder(0, null, 0, null, null, null); + var elBinder = new ElementBinder(0, null, 0, null, null); elBinder.nestedProtoView = nestedProtoView; return elBinder; } diff --git a/modules/angular2/test/core/compiler/element_injector_spec.ts b/modules/angular2/test/core/compiler/element_injector_spec.ts index 134d16c6f7..7505c582f2 100644 --- a/modules/angular2/test/core/compiler/element_injector_spec.ts +++ b/modules/angular2/test/core/compiler/element_injector_spec.ts @@ -163,6 +163,12 @@ class NeedsQuery { constructor(@Query(CountingDirective) query: QueryList) { this.query = query; } } +@Injectable() +class NeedsQueryByVarBindings { + query: QueryList; + constructor(@Query("one,two") query: QueryList) { this.query = query; } +} + @Injectable() class NeedsElementRef { elementRef; @@ -229,13 +235,13 @@ export function main() { dynamicBindings.push(bind(i).toValue(i)); } - function createPei(parent, index, bindings, distance = 1, hasShadowRoot = false) { + function createPei(parent, index, bindings, distance = 1, hasShadowRoot = false, dirVariableBindings = null) { var directiveBinding = ListWrapper.map(bindings, b => { if (b instanceof DirectiveBinding) return b; if (b instanceof Binding) return DirectiveBinding.createFromBinding(b, null); return DirectiveBinding.createFromType(b, null); }); - return ProtoElementInjector.create(parent, index, directiveBinding, hasShadowRoot, distance); + return ProtoElementInjector.create(parent, index, directiveBinding, hasShadowRoot, distance, dirVariableBindings); } function humanize(tree: TreeNode, names: List>) { @@ -248,10 +254,10 @@ export function main() { } function injector(bindings, lightDomAppInjector = null, isComponent: boolean = false, - preBuiltObjects = null, attributes = null) { + preBuiltObjects = null, attributes = null, dirVariableBindings = null) { if (isBlank(lightDomAppInjector)) lightDomAppInjector = appInjector; - var proto = createPei(null, 0, bindings, 0, isComponent); + var proto = createPei(null, 0, bindings, 0, isComponent, dirVariableBindings); proto.attributes = attributes; var inj = proto.instantiate(null); @@ -942,7 +948,7 @@ export function main() { }); }); - describe('directive queries', () => { + describe('queries', () => { var preBuildObjects = defaultPreBuiltObjects; beforeEach(() => { _constructionCount = 0; }); @@ -963,9 +969,39 @@ export function main() { it('should contain directives on the same injector', () => { var inj = injector(ListWrapper.concat([NeedsQuery, CountingDirective], extraBindings), null, - false, preBuildObjects); + false, preBuildObjects); expectDirectives(inj.get(NeedsQuery).query, CountingDirective, [0]); + }) + + it('should contain the element when no directives are bound to the var binding', () => { + var dirs = [NeedsQueryByVarBindings]; + + var dirVariableBindings = MapWrapper.createFromStringMap({ + "one": null // element + }); + + var inj = injector(ListWrapper.concat(dirs, extraBindings), null, + false, preBuildObjects, null, dirVariableBindings); + + expect(inj.get(NeedsQueryByVarBindings).query.first).toBeAnInstanceOf(ElementRef); + }); + + it('should contain directives on the same injector when querying by variable bindings' + + 'in the order of var bindings specified in the query', () => { + var dirs = [NeedsQueryByVarBindings, NeedsDirective, SimpleDirective]; + + var dirVariableBindings = MapWrapper.createFromStringMap({ + "one": 2, // 2 is the index of SimpleDirective + "two": 1 // 1 is the index of NeedsDirective + }); + + var inj = injector(ListWrapper.concat(dirs, extraBindings), null, + false, preBuildObjects, null, dirVariableBindings); + + // 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. diff --git a/modules/angular2/test/core/compiler/query_integration_spec.ts b/modules/angular2/test/core/compiler/query_integration_spec.ts index 58c91bbab6..d582bada5d 100644 --- a/modules/angular2/test/core/compiler/query_integration_spec.ts +++ b/modules/angular2/test/core/compiler/query_integration_spec.ts @@ -26,154 +26,247 @@ export function main() { BrowserDomAdapter.makeCurrent(); describe('Query API', () => { - it('should contain all direct child directives in the light dom', - inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
' + - '
' + - '
' + - '
' + - '
'; + describe("querying by directive type", () => { + it('should contain all direct child directives in the light dom', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
' + + '
' + + '
' + + '
' + + '
'; - tb.createView(MyComp, {html: template}) - .then((view) => { - view.detectChanges(); - expect(view.rootNodes).toHaveText('2|3|'); + tb.createView(MyComp, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes).toHaveText('2|3|'); - async.done(); - }); - })); - - it('should contain all directives in the light dom when descendants flag is used', - inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
' + - '
' + - '
' + - '
' + - '
'; - - tb.createView(MyComp, {html: template}) - .then((view) => { - view.detectChanges(); - expect(view.rootNodes).toHaveText('2|3|4|'); - - async.done(); - }); - })); - - it('should contain all directives in the light dom', - inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
' + - '
' + - '
'; - - tb.createView(MyComp, {html: template}) - .then((view) => { - view.detectChanges(); - expect(view.rootNodes).toHaveText('2|3|'); - - async.done(); - }); - })); - - // TODO(rado): The test below should be using descendants: false, - // but due to a bug with how injectors are hooked up query considers the - // directives to be distances 2 instead of direct children. - it('should reflect dynamically inserted directives', - inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = - '
' + - '
' + - '
'; - - tb.createView(MyComp, {html: template}) - .then((view) => { - - view.detectChanges(); - expect(view.rootNodes).toHaveText('2|'); - - view.context.shouldShow = true; - view.detectChanges(); - expect(view.rootNodes).toHaveText('2|3|'); - - async.done(); - }); - })); - - it('should reflect moved directives', - inject([TestBed, AsyncTestCompleter], (tb: TestBed, 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(); - }); - })); - - - it('should notify query on change', - inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '' + - '
' + - '
' + - '
'; - - tb.createView(MyComp, {html: template}) - .then((view) => { - var q = view.rawView.locals.get("q"); - view.detectChanges(); - - q.query.onChange(() => { - expect(q.query.first.text).toEqual("1"); - expect(q.query.last.text).toEqual("2"); async.done(); }); + })); - view.context.shouldShow = true; - view.detectChanges(); - }); - })); + it('should contain all directives in the light dom when descendants flag is used', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
' + + '
' + + '
' + + '
' + + '
'; - it("should notify child's query before notifying parent's query", - inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '' + - '' + - '
' + - '
' + - '
'; + tb.createView(MyComp, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes).toHaveText('2|3|4|'); - tb.createView(MyComp, {html: template}) - .then((view) => { - var q1 = view.rawView.locals.get("q1"); - var q2 = view.rawView.locals.get("q2"); - - var firedQ2 = false; - - q2.query.onChange(() => { firedQ2 = true; }); - q1.query.onChange(() => { - expect(firedQ2).toBe(true); async.done(); }); + })); - view.detectChanges(); - }); - })); + it('should contain all directives in the light dom', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes).toHaveText('2|3|'); + + async.done(); + }); + })); + + // TODO(rado): The test below should be using descendants: false, + // but due to a bug with how injectors are hooked up query considers the + // directives to be distances 2 instead of direct children. + it('should reflect dynamically inserted directives', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = + '
' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + + view.detectChanges(); + expect(view.rootNodes).toHaveText('2|'); + + view.context.shouldShow = true; + view.detectChanges(); + expect(view.rootNodes).toHaveText('2|3|'); + + async.done(); + }); + })); + + it('should reflect moved directives', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = + '
' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + 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(); + }); + })); + }); + + describe("onChange", () => { + it('should notify query on change', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '' + + '
' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + var q = view.rawView.locals.get("q"); + view.detectChanges(); + + q.query.onChange(() => { + expect(q.query.first.text).toEqual("1"); + expect(q.query.last.text).toEqual("2"); + async.done(); + }); + + view.context.shouldShow = true; + view.detectChanges(); + }); + })); + + it("should notify child's query before notifying parent's query", + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '' + + '' + + '
' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + var q1 = view.rawView.locals.get("q1"); + var q2 = view.rawView.locals.get("q2"); + + var firedQ2 = false; + + q2.query.onChange(() => { firedQ2 = true; }); + q1.query.onChange(() => { + expect(firedQ2).toBe(true); + async.done(); + }); + + view.detectChanges(); + }); + })); + }); + + describe("querying by var binding", () => { + it('should contain all the child directives in the light dom with the given var binding', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = + '' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + var q = view.rawView.locals.get("q"); + + view.context.list = ['1d', '2d']; + + view.detectChanges(); + + expect(q.query.first.text).toEqual("1d"); + expect(q.query.last.text).toEqual("2d"); + + async.done(); + }); + })); + + it('should support querying by multiple var bindings', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '' + + '
' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + var q = view.rawView.locals.get("q"); + view.detectChanges(); + + expect(q.query.first.text).toEqual("one"); + expect(q.query.last.text).toEqual("two"); + + async.done(); + }); + })); + + it('should reflect dynamically inserted directives', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = + '' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + var q = view.rawView.locals.get("q"); + + view.context.list = ['1d', '2d']; + + view.detectChanges(); + + view.context.list = ['2d', '1d']; + + view.detectChanges(); + + expect(q.query.last.text).toEqual("1d"); + + async.done(); + }); + })); + + it('should contain all the elements in the light dom with the given var binding', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '' + + '
' + + '
{{item}}
' + + '
' + + '
'; + + tb.createView(MyComp, {html: template}) + .then((view) => { + var q = view.rawView.locals.get("q"); + + view.context.list = ['1d', '2d']; + + view.detectChanges(); + + expect(q.query.first.domElement).toHaveText("1d"); + expect(q.query.last.domElement).toHaveText("2d"); + + async.done(); + }); + })); + }); }); } -@Directive({selector: '[text]', properties: ['text']}) +@Directive({selector: '[text]', properties: ['text'], exportAs: 'textDir'}) @Injectable() class TextDirective { text: string; @@ -198,8 +291,38 @@ class NeedsQueryDesc { } } +@Component({selector: 'needs-query-by-var-binding'}) +@View({directives: [], template: ''}) +@Injectable() +class NeedsQueryByLabel { + query: QueryList; + constructor(@Query("textLabel", {descendants: true}) query: QueryList) { + this.query = query; + } +} + +@Component({selector: 'needs-query-by-var-bindings'}) +@View({directives: [], template: ''}) +@Injectable() +class NeedsQueryByTwoLabels { + query: QueryList; + constructor(@Query("textLabel1,textLabel2", {descendants: true}) query: QueryList) { + this.query = query; + } +} + @Component({selector: 'my-comp'}) -@View({directives: [NeedsQuery, NeedsQueryDesc, TextDirective, NgIf, NgFor]}) +@View({ + directives: [ + NeedsQuery, + NeedsQueryDesc, + NeedsQueryByLabel, + NeedsQueryByTwoLabels, + TextDirective, + NgIf, + NgFor + ] +}) @Injectable() class MyComp { shouldShow: boolean; diff --git a/modules/angular2/test/core/compiler/view_manager_spec.ts b/modules/angular2/test/core/compiler/view_manager_spec.ts index 561e92af55..f76824d49c 100644 --- a/modules/angular2/test/core/compiler/view_manager_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_spec.ts @@ -58,11 +58,11 @@ export function main() { return DirectiveBinding.createFromType(type, annotation); } - function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null, null); } + function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null); } function createComponentElBinder(nestedProtoView = null) { var binding = createDirectiveBinding(SomeComponent); - var binder = new ElementBinder(0, null, 0, null, null, binding); + var binder = new ElementBinder(0, null, 0, null, binding); binder.nestedProtoView = nestedProtoView; return binder; } 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 66254b4071..e4354b61c7 100644 --- a/modules/angular2/test/core/compiler/view_manager_utils_spec.ts +++ b/modules/angular2/test/core/compiler/view_manager_utils_spec.ts @@ -48,11 +48,11 @@ export function main() { return DirectiveBinding.createFromType(type, annotation); } - function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null, null); } + function createEmptyElBinder() { return new ElementBinder(0, null, 0, null, null); } function createComponentElBinder(nestedProtoView = null) { var binding = createDirectiveBinding(SomeComponent); - var binder = new ElementBinder(0, null, 0, null, null, binding); + var binder = new ElementBinder(0, null, 0, null, binding); binder.nestedProtoView = nestedProtoView; return binder; } diff --git a/modules/benchmarks/src/element_injector/element_injector_benchmark.ts b/modules/benchmarks/src/element_injector/element_injector_benchmark.ts index edc65a5465..3082a13c08 100644 --- a/modules/benchmarks/src/element_injector/element_injector_benchmark.ts +++ b/modules/benchmarks/src/element_injector/element_injector_benchmark.ts @@ -19,7 +19,7 @@ export function main() { DirectiveBinding.createFromType(B, null), DirectiveBinding.createFromType(C, null) ]; - var proto = ProtoElementInjector.create(null, 0, bindings, false, 0); + var proto = ProtoElementInjector.create(null, 0, bindings, false, 0, null); var elementInjector = proto.instantiate(null); function instantiate() {