From 7ee6963f5d810e26c1476c9225708bfddc7f2ce7 Mon Sep 17 00:00:00 2001 From: Rado Kirov Date: Fri, 10 Jul 2015 10:30:31 -0700 Subject: [PATCH] feat(query): initial implementation of view query. ViewQuery is a new API that allows a component to query its view. Closes #1935 --- modules/angular2/annotations.ts | 3 +- .../src/core/annotations/decorators.ts | 8 +- modules/angular2/src/core/annotations/di.ts | 1 + .../angular2/src/core/annotations_impl/di.ts | 19 +++ .../src/core/compiler/base_query_list.dart | 5 + .../src/core/compiler/base_query_list.ts | 2 + .../src/core/compiler/element_injector.ts | 28 +++- .../core/compiler/query_integration_spec.ts | 156 +++++++++++++++++- .../test/core/compiler/query_list_spec.ts | 11 +- 9 files changed, 223 insertions(+), 10 deletions(-) diff --git a/modules/angular2/annotations.ts b/modules/angular2/annotations.ts index aa742d019a..ffa5d5663e 100644 --- a/modules/angular2/annotations.ts +++ b/modules/angular2/annotations.ts @@ -53,5 +53,6 @@ export { ViewDecorator, ViewFactory, Query, - QueryFactory + QueryFactory, + ViewQuery } from 'angular2/src/core/annotations/decorators'; diff --git a/modules/angular2/src/core/annotations/decorators.ts b/modules/angular2/src/core/annotations/decorators.ts index 6af0b1f620..b18485ea82 100644 --- a/modules/angular2/src/core/annotations/decorators.ts +++ b/modules/angular2/src/core/annotations/decorators.ts @@ -1,6 +1,6 @@ import {ComponentAnnotation, DirectiveAnnotation, LifecycleEvent} from './annotations'; import {ViewAnnotation} from './view'; -import {AttributeAnnotation, QueryAnnotation} from './di'; +import {AttributeAnnotation, QueryAnnotation, ViewQueryAnnotation} from './di'; import { makeDecorator, makeParamDecorator, @@ -368,3 +368,9 @@ export var Attribute: AttributeFactory = makeParamDecorator(AttributeAnnotation) * {@link Query} factory function. */ export var Query: QueryFactory = makeParamDecorator(QueryAnnotation); + + +/** + * {@link ViewQuery} factory function. + */ +export var ViewQuery: QueryFactory = makeParamDecorator(ViewQueryAnnotation); diff --git a/modules/angular2/src/core/annotations/di.ts b/modules/angular2/src/core/annotations/di.ts index f723097863..61a23c1c9b 100644 --- a/modules/angular2/src/core/annotations/di.ts +++ b/modules/angular2/src/core/annotations/di.ts @@ -1,4 +1,5 @@ export { Query as QueryAnnotation, + ViewQuery as ViewQueryAnnotation, Attribute as AttributeAnnotation, } from '../annotations_impl/di'; diff --git a/modules/angular2/src/core/annotations_impl/di.ts b/modules/angular2/src/core/annotations_impl/di.ts index 7b0d385293..942d3c5186 100644 --- a/modules/angular2/src/core/annotations_impl/di.ts +++ b/modules/angular2/src/core/annotations_impl/di.ts @@ -57,6 +57,8 @@ export class Query extends DependencyMetadata { this.descendants = descendants; } + get isViewQuery() { return false; } + get selector() { return resolveForwardRef(this._selector); } get isVarBindingQuery(): boolean { return isString(this.selector); } @@ -65,3 +67,20 @@ export class Query extends DependencyMetadata { toString(): string { return `@Query(${stringify(this.selector)})`; } } + +/** + * Specifies that a {@link QueryList} should be injected. + * + * See {@link QueryList} for usage and example. + * + * @exportedAs angular2/annotations + */ +@CONST() +export class ViewQuery extends Query { + constructor(_selector: Type | string, {descendants = false}: {descendants?: boolean} = {}) { + super(_selector, {descendants: descendants}); + } + + get isViewQuery() { return true; } + toString(): string { return `@ViewQuery(${stringify(this.selector)})`; } +} diff --git a/modules/angular2/src/core/compiler/base_query_list.dart b/modules/angular2/src/core/compiler/base_query_list.dart index 1b1d53bc74..5bd8fcfb04 100644 --- a/modules/angular2/src/core/compiler/base_query_list.dart +++ b/modules/angular2/src/core/compiler/base_query_list.dart @@ -45,4 +45,9 @@ class BaseQueryList extends Object with IterableMixin { get length => _results.length; get first => _results.first; get last => _results.last; + + List map(fn(T)) { + // Note: we need to return a list instead of iterable to match JS. + return this._results.map(fn).toList(); + } } diff --git a/modules/angular2/src/core/compiler/base_query_list.ts b/modules/angular2/src/core/compiler/base_query_list.ts index 5ac37f0d75..3ed5aedf69 100644 --- a/modules/angular2/src/core/compiler/base_query_list.ts +++ b/modules/angular2/src/core/compiler/base_query_list.ts @@ -38,4 +38,6 @@ export class BaseQueryList { get length() { return this._results.length; } get first() { return ListWrapper.first(this._results); } get last() { return ListWrapper.last(this._results); } + + map(fn: (T) => U): U[] { return this._results.map(fn); } } diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index 5bd24ae844..53638b0e4b 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -434,7 +434,7 @@ export class ElementInjector extends TreeNode implements Depend private _preBuiltObjects = null; // Queries are added during construction or linking with a new parent. - // They are never removed. + // They are removed only through unlinking. private _query0: QueryRef; private _query1: QueryRef; private _query2: QueryRef; @@ -487,6 +487,10 @@ export class ElementInjector extends TreeNode implements Depend this._hydrateInjector(imperativelyCreatedInjector, host); + if (isPresent(host)) { + this._addViewQueries(host); + } + this._addDirectivesToQueries(); this._addVarBindingsToQueries(); @@ -650,6 +654,22 @@ export class ElementInjector extends TreeNode implements Depend } } + private _addViewQueries(host: ElementInjector): void { + if (isPresent(host._query0) && host._query0.originator == host) + this._addViewQuery(host._query0); + if (isPresent(host._query1) && host._query1.originator == host) + this._addViewQuery(host._query1); + if (isPresent(host._query2) && host._query2.originator == host) + this._addViewQuery(host._query2); + } + + private _addViewQuery(queryRef: QueryRef): void { + // 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); @@ -733,15 +753,15 @@ export class ElementInjector extends TreeNode implements Depend private _addParentQueries(): void { if (isBlank(this.parent)) return; - if (isPresent(this.parent._query0)) { + if (isPresent(this.parent._query0) && !this.parent._query0.query.isViewQuery) { this._addQueryToTree(this.parent._query0); if (this.hydrated) this.parent._query0.update(); } - if (isPresent(this.parent._query1)) { + if (isPresent(this.parent._query1) && !this.parent._query1.query.isViewQuery) { this._addQueryToTree(this.parent._query1); if (this.hydrated) this.parent._query1.update(); } - if (isPresent(this.parent._query2)) { + if (isPresent(this.parent._query2) && !this.parent._query2.query.isViewQuery) { this._addQueryToTree(this.parent._query2); if (this.hydrated) this.parent._query2.update(); } diff --git a/modules/angular2/test/core/compiler/query_integration_spec.ts b/modules/angular2/test/core/compiler/query_integration_spec.ts index f11598b614..14ef1c5dfc 100644 --- a/modules/angular2/test/core/compiler/query_integration_spec.ts +++ b/modules/angular2/test/core/compiler/query_integration_spec.ts @@ -16,10 +16,10 @@ import { import {Injectable, Optional} from 'angular2/di'; import {QueryList} from 'angular2/core'; -import {Query, Component, Directive, View} from 'angular2/annotations'; +import {Query, ViewQuery, Component, Directive, View} from 'angular2/annotations'; import {NgIf, NgFor} from 'angular2/angular2'; -import {ListWrapper} from 'angular2/src/facade/collection'; +import {ListWrapper, iterableToList} from 'angular2/src/facade/collection'; import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter'; @@ -273,6 +273,102 @@ export function main() { }); })); }); + + describe("querying in the view", () => { + it('should contain all the elements in the view with that have the given directive', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var template = '
'; + + tcb.overrideTemplate(MyComp, template) + .createAsync(MyComp) + .then((view) => { + var q: NeedsViewQuery = view.componentViewChildren[0].getLocal("q"); + + 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', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var template = ''; + + tcb.overrideTemplate(MyComp, template) + .createAsync(MyComp) + .then((view) => { + var q: NeedsViewQuery = view.componentViewChildren[0].getLocal("q"); + + view.detectChanges(); + + expect(q.query.map((d: TextDirective) => d.text)).toEqual(["self", "1", "2", "3"]); + + async.done(); + }); + })); + + it('should reflect changes in the component', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var template = ''; + + tcb.overrideTemplate(MyComp, template) + .createAsync(MyComp) + .then((view) => { + var q: NeedsViewQueryIf = view.componentViewChildren[0].getLocal("q"); + + view.detectChanges(); + + expect(q.query.length).toBe(0); + + q.show = true; + view.detectChanges(); + + expect(q.query.first.text).toEqual("1"); + + async.done(); + }); + })); + + /* TODO(rado): fix and reenable. + + 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:NeedsViewQueryOrder = view.componentViewChildren[0].getLocal("q"); + + view.detectChanges(); + + expect(q.query.length).toBe(4); + expect(q.query.first.text).toEqual("1"); + expect(q.query.first.text).toEqual("4"); + + async.done(); + }); + }));*/ + }); }); } @@ -321,6 +417,58 @@ class NeedsQueryByTwoLabels { } } +@Component({selector: 'needs-view-query'}) +@View({ + directives: [TextDirective], + template: '
' + + '
' +}) +@Injectable() +class NeedsViewQuery { + query: QueryList; + 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() +class NeedsViewQueryIf { + show: boolean; + query: QueryList; + constructor(@ViewQuery(TextDirective) query: QueryList) { + this.query = query; + this.show = false; + } +} + + +@Component({selector: 'needs-view-query-order'}) +@View({ + directives: [NgFor, TextDirective], + template: '
' + + '
' + + '
; + constructor(@ViewQuery(TextDirective) query: QueryList) { this.query = query; } +} + @Component({selector: 'my-comp'}) @View({ directives: [ @@ -328,6 +476,10 @@ class NeedsQueryByTwoLabels { NeedsQueryDesc, NeedsQueryByLabel, NeedsQueryByTwoLabels, + NeedsViewQuery, + NeedsViewQueryDesc, + NeedsViewQueryIf, + NeedsViewQueryOrder, TextDirective, NgIf, NgFor diff --git a/modules/angular2/test/core/compiler/query_list_spec.ts b/modules/angular2/test/core/compiler/query_list_spec.ts index 2d26b2126a..5a8684ebb9 100644 --- a/modules/angular2/test/core/compiler/query_list_spec.ts +++ b/modules/angular2/test/core/compiler/query_list_spec.ts @@ -6,9 +6,10 @@ import {QueryList} from 'angular2/src/core/compiler/query_list'; export function main() { describe('QueryList', () => { - var queryList, log; + var queryList: QueryList; + var log: string; beforeEach(() => { - queryList = new QueryList(); + queryList = new QueryList(); log = ''; }); @@ -36,6 +37,12 @@ export function main() { expect(queryList.length).toEqual(2); }); + it('should support map', () => { + queryList.add('one'); + queryList.add('two'); + expect(queryList.map((x) => x)).toEqual(['one', 'two']); + }); + it('should support first and last', () => { queryList.add('one'); queryList.add('two');