From c2a60f1624ad54c424a2a9f6425420edd84e97b8 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Sat, 19 Sep 2015 18:39:35 -0700 Subject: [PATCH] feat(core): add support for @ContentChild and @ViewChild Closes #4251 --- .../src/core/compiler/directive_resolver.ts | 14 +- .../src/core/compiler/element_injector.ts | 6 +- modules/angular2/src/core/metadata.dart | 16 +++ modules/angular2/src/core/metadata.ts | 31 ++++- modules/angular2/src/core/metadata/di.ts | 61 +++++++- .../core/compiler/directive_resolver_spec.ts | 30 +++- .../core/compiler/query_integration_spec.ts | 130 ++++++++++++++++-- modules/angular2/test/public_api_spec.ts | 48 +++++++ 8 files changed, 313 insertions(+), 23 deletions(-) diff --git a/modules/angular2/src/core/compiler/directive_resolver.ts b/modules/angular2/src/core/compiler/directive_resolver.ts index 49819bfd08..53a70fec3e 100644 --- a/modules/angular2/src/core/compiler/directive_resolver.ts +++ b/modules/angular2/src/core/compiler/directive_resolver.ts @@ -10,11 +10,13 @@ import { HostBindingMetadata, HostListenerMetadata, ContentChildrenMetadata, - ViewChildrenMetadata + ViewChildrenMetadata, + ContentChildMetadata, + ViewChildMetadata } from 'angular2/src/core/metadata'; import {reflector} from 'angular2/src/core/reflection/reflection'; -/** +/* * Resolve a `Type` for {@link DirectiveMetadata}. * * This interface can be overridden by the application developer to create custom behavior. @@ -86,6 +88,14 @@ export class DirectiveResolver { if (a instanceof ViewChildrenMetadata) { queries[propName] = a; } + + if (a instanceof ContentChildMetadata) { + queries[propName] = a; + } + + if (a instanceof ViewChildMetadata) { + queries[propName] = a; + } }); }); return this._merge(dm, properties, events, host, queries); diff --git a/modules/angular2/src/core/compiler/element_injector.ts b/modules/angular2/src/core/compiler/element_injector.ts index d3e8eb6d66..50051c1bd6 100644 --- a/modules/angular2/src/core/compiler/element_injector.ts +++ b/modules/angular2/src/core/compiler/element_injector.ts @@ -967,7 +967,11 @@ export class QueryRef { // TODO delete the check once only field queries are supported if (isPresent(this.dirIndex)) { var dir = this.originator.getDirectiveAtIndex(this.dirIndex); - this.setter(dir, this.list); + if (this.query.first) { + this.setter(dir, this.list.length > 0 ? this.list.first : null); + } else { + this.setter(dir, this.list); + } } } diff --git a/modules/angular2/src/core/metadata.dart b/modules/angular2/src/core/metadata.dart index 06df4f3fae..1301849f91 100644 --- a/modules/angular2/src/core/metadata.dart +++ b/modules/angular2/src/core/metadata.dart @@ -102,6 +102,14 @@ class ContentChildren extends ContentChildrenMetadata { : super(selector, descendants: descendants); } +/** + * See: [ContentChildMetadata] for docs. + */ +class ContentChild extends ContentChildMetadata { + const ContentChild(dynamic /*Type | string*/ selector) + : super(selector); +} + /** * See: [ViewQueryMetadata] for docs. */ @@ -118,6 +126,14 @@ class ViewChildren extends ViewChildrenMetadata { : super(selector); } +/** + * See: [ViewChildMetadata] for docs. + */ +class ViewChild extends ViewChildMetadata { + const ViewChild(dynamic /*Type | string*/ selector) + : super(selector); +} + /** * See: [PropertyMetadata] for docs. */ diff --git a/modules/angular2/src/core/metadata.ts b/modules/angular2/src/core/metadata.ts index dd0070807c..75c596a459 100644 --- a/modules/angular2/src/core/metadata.ts +++ b/modules/angular2/src/core/metadata.ts @@ -6,9 +6,11 @@ export { QueryMetadata, ContentChildrenMetadata, + ContentChildMetadata, ViewChildrenMetadata, ViewQueryMetadata, - AttributeMetadata, + ViewChildMetadata, + AttributeMetadata } from './metadata/di'; export { @@ -26,9 +28,11 @@ export {ViewMetadata, ViewEncapsulation} from './metadata/view'; import { QueryMetadata, ContentChildrenMetadata, + ContentChildMetadata, ViewChildrenMetadata, + ViewChildMetadata, ViewQueryMetadata, - AttributeMetadata, + AttributeMetadata } from './metadata/di'; import { @@ -408,11 +412,22 @@ export interface ContentChildrenFactory { new (selector: Type | string, {descendants}?: {descendants?: boolean}): ContentChildrenMetadata; } +export interface ContentChildFactory { + (selector: Type | string): any; + new (selector: Type | string): ContentChildFactory; +} + export interface ViewChildrenFactory { (selector: Type | string): any; new (selector: Type | string): ViewChildrenMetadata; } +export interface ViewChildFactory { + (selector: Type | string): any; + new (selector: Type | string): ViewChildFactory; +} + + /** * {@link PipeMetadata} factory for creating decorators. * @@ -546,13 +561,23 @@ export var Query: QueryFactory = makeParamDecorator(QueryMetadata); */ export var ContentChildren: ContentChildrenFactory = makePropDecorator(ContentChildrenMetadata); +/** + * {@link ContentChildMetadata} factory function. + */ +export var ContentChild: ContentChildFactory = makePropDecorator(ContentChildMetadata); + /** * {@link ViewChildrenMetadata} factory function. */ export var ViewChildren: ViewChildrenFactory = makePropDecorator(ViewChildrenMetadata); /** - * {@link ViewQueryMetadata} factory function. + * {@link ViewChildMetadata} factory function. + */ +export var ViewChild: ViewChildFactory = makePropDecorator(ViewChildMetadata); + +/** + * {@link di/ViewQueryMetadata} factory function. */ export var ViewQuery: QueryFactory = makeParamDecorator(ViewQueryMetadata); diff --git a/modules/angular2/src/core/metadata/di.ts b/modules/angular2/src/core/metadata/di.ts index 85539047ef..13fdd8a258 100644 --- a/modules/angular2/src/core/metadata/di.ts +++ b/modules/angular2/src/core/metadata/di.ts @@ -170,11 +170,13 @@ export class QueryMetadata extends DependencyMetadata { * children (true). */ descendants: boolean; + first: boolean; constructor(private _selector: Type | string, - {descendants = false}: {descendants?: boolean} = {}) { + {descendants = false, first = false}: {descendants?: boolean, first?: boolean} = {}) { super(); this.descendants = descendants; + this.first = first; } /** @@ -229,6 +231,32 @@ export class ContentChildrenMetadata extends QueryMetadata { } } +// TODO: add an example after ContentChild and ViewChild are in master +/** + * Configures a content query. + * + * Content queries are set before the `afterContentInit` callback is called. + * + * ### Example + * + * ``` + * @Directive({ + * selector: 'someDir' + * }) + * class SomeDir { + * @ContentChild(ChildDirective) contentChild; + * + * afterContentInit() { + * // contentChild is set + * } + * } + * ``` + */ +@CONST() +export class ContentChildMetadata extends QueryMetadata { + constructor(_selector: Type | string) { super(_selector, {descendants: true, first: true}); } +} + /** * Similar to {@link QueryMetadata}, but querying the component view, instead of * the content children. @@ -266,8 +294,9 @@ export class ContentChildrenMetadata extends QueryMetadata { */ @CONST() export class ViewQueryMetadata extends QueryMetadata { - constructor(_selector: Type | string, {descendants = false}: {descendants?: boolean} = {}) { - super(_selector, {descendants: descendants}); + constructor(_selector: Type | string, + {descendants = false, first = false}: {descendants?: boolean, first?: boolean} = {}) { + super(_selector, {descendants: descendants, first: first}); } /** @@ -302,3 +331,29 @@ export class ViewQueryMetadata extends QueryMetadata { export class ViewChildrenMetadata extends ViewQueryMetadata { constructor(_selector: Type | string) { super(_selector, {descendants: true}); } } + +/** + * Configures a view query. + * + * View queries are set before the `afterViewInit` callback is called. + * + * ### Example + * + * ``` + * @Component({ + * selector: 'someDir' + * }) + * @View({templateUrl: 'someTemplate', directives: [ItemDirective]}) + * class SomeDir { + * @ViewChild(ItemDirective) viewChild:ItemDirective; + * + * afterViewInit() { + * // viewChild is set + * } + * } + * ``` + */ +@CONST() +export class ViewChildMetadata extends ViewQueryMetadata { + constructor(_selector: Type | string) { super(_selector, {descendants: true, first: true}); } +} \ No newline at end of file diff --git a/modules/angular2/test/core/compiler/directive_resolver_spec.ts b/modules/angular2/test/core/compiler/directive_resolver_spec.ts index f2550ba023..ef80e60a61 100644 --- a/modules/angular2/test/core/compiler/directive_resolver_spec.ts +++ b/modules/angular2/test/core/compiler/directive_resolver_spec.ts @@ -10,7 +10,11 @@ import { ContentChildren, ContentChildrenMetadata, ViewChildren, - ViewChildrenMetadata + ViewChildrenMetadata, + ContentChild, + ContentChildMetadata, + ViewChild, + ViewChildMetadata } from 'angular2/src/core/metadata'; @Directive({selector: 'someDirective'}) @@ -80,6 +84,18 @@ class SomeDirectiveWithViewChildren { c; } +@Directive({selector: 'someDirective', queries: {"c": new ContentChild("c")}}) +class SomeDirectiveWithContentChild { + @ContentChild("a") a: any; + c; +} + +@Directive({selector: 'someDirective', queries: {"c": new ViewChild("c")}}) +class SomeDirectiveWithViewChild { + @ViewChild("a") a: any; + c; +} + class SomeDirectiveWithoutMetadata {} export function main() { @@ -156,6 +172,18 @@ export function main() { expect(directiveMetadata.queries) .toEqual({"cs": new ViewChildren("c"), "as": new ViewChildren("a")}); }); + + it('should append ContentChild', () => { + var directiveMetadata = resolver.resolve(SomeDirectiveWithContentChild); + expect(directiveMetadata.queries) + .toEqual({"c": new ContentChild("c"), "a": new ContentChild("a")}); + }); + + it('should append ViewChild', () => { + var directiveMetadata = resolver.resolve(SomeDirectiveWithViewChild); + expect(directiveMetadata.queries) + .toEqual({"c": new ViewChild("c"), "a": new ViewChild("a")}); + }); }); }); } diff --git a/modules/angular2/test/core/compiler/query_integration_spec.ts b/modules/angular2/test/core/compiler/query_integration_spec.ts index 00e22f6c2e..f2a45e11ed 100644 --- a/modules/angular2/test/core/compiler/query_integration_spec.ts +++ b/modules/angular2/test/core/compiler/query_integration_spec.ts @@ -12,6 +12,7 @@ import { TestComponentBuilder, } from 'angular2/test_lib'; +import {isPresent} from 'angular2/src/core/facade/lang'; import { Component, @@ -27,8 +28,12 @@ import { ViewQuery, ContentChildren, ViewChildren, + ContentChild, + ViewChild, AfterContentInit, - AfterViewInit + AfterViewInit, + AfterContentChecked, + AfterViewChecked } from 'angular2/core'; import {asNativeElements} from 'angular2/src/core/debug'; @@ -81,6 +86,63 @@ export function main() { }); })); + it('should contain the first content child', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var template = + '
'; + + tcb.overrideTemplate(MyComp, template) + .createAsync(MyComp) + .then((view) => { + view.debugElement.componentInstance.shouldShow = true; + view.detectChanges(); + + var q = view.debugElement.componentViewChildren[0].getLocal('q'); + + expect(q.log).toEqual([["setter", "foo"], ["init", "foo"], ["check", "foo"]]); + + view.debugElement.componentInstance.shouldShow = false; + view.detectChanges(); + + expect(q.log).toEqual([ + ["setter", "foo"], + ["init", "foo"], + ["check", "foo"], + ["setter", null], + ["check", null] + ]); + + async.done(); + }); + })); + + it('should contain the first view child', + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { + var template = ''; + + tcb.overrideTemplate(MyComp, template) + .createAsync(MyComp) + .then((view) => { + view.detectChanges(); + var q = view.debugElement.componentViewChildren[0].getLocal('q'); + + expect(q.log).toEqual([["setter", "foo"], ["init", "foo"], ["check", "foo"]]); + + q.shouldShow = false; + view.detectChanges(); + + expect(q.log).toEqual([ + ["setter", "foo"], + ["init", "foo"], + ["check", "foo"], + ["setter", null], + ["check", null] + ]); + + async.done(); + }); + })); + it('should contain all directives in the light dom when descendants flag is used', inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => { var template = '
' + @@ -598,32 +660,72 @@ class TextDirective { constructor() {} } -@Component({ - selector: 'needs-content-children', - queries: {'textDirChildren': new ContentChildren(TextDirective)} -}) +@Component({selector: 'needs-content-children'}) @View({template: ''}) class NeedsContentChildren implements AfterContentInit { - textDirChildren: QueryList; + @ContentChildren(TextDirective) textDirChildren: QueryList; numberOfChildrenAfterContentInit: number; afterContentInit() { this.numberOfChildrenAfterContentInit = this.textDirChildren.length; } } -@Component({ - selector: 'needs-view-children', - queries: { - 'textDirChildren': new ViewChildren(TextDirective), - } -}) +@Component({selector: 'needs-view-children'}) @View({template: '
', directives: [TextDirective]}) class NeedsViewChildren implements AfterViewInit { - textDirChildren: QueryList; + @ViewChildren(TextDirective) textDirChildren: QueryList; numberOfChildrenAfterViewInit: number; afterViewInit() { this.numberOfChildrenAfterViewInit = this.textDirChildren.length; } } +@Component({selector: 'needs-content-child'}) +@View({template: ''}) +class NeedsContentChild implements AfterContentInit, AfterContentChecked { + _child: TextDirective; + + @ContentChild(TextDirective) + set child(value) { + this._child = value; + this.log.push(['setter', isPresent(value) ? value.text : null]); + } + + get child() { return this._child; } + log = []; + + afterContentInit() { this.log.push(["init", isPresent(this.child) ? this.child.text : null]); } + + afterContentChecked() { + this.log.push(["check", isPresent(this.child) ? this.child.text : null]); + } +} + +@Component({selector: 'needs-view-child'}) +@View({ + template: ` +
+ `, + directives: [NgIf, TextDirective] +}) +class NeedsViewChild implements AfterViewInit, + AfterViewChecked { + shouldShow: boolean = true; + _child: TextDirective; + + @ViewChild(TextDirective) + set child(value) { + this._child = value; + this.log.push(['setter', isPresent(value) ? value.text : null]); + } + + get child() { return this._child; } + log = []; + + afterViewInit() { this.log.push(["init", isPresent(this.child) ? this.child.text : null]); } + + afterViewChecked() { this.log.push(["check", isPresent(this.child) ? this.child.text : null]); } +} + + @Directive({selector: '[dir]'}) @Injectable() class InertDirective { @@ -792,6 +894,8 @@ class NeedsTpl { NeedsViewQueryOrderWithParent, NeedsContentChildren, NeedsViewChildren, + NeedsViewChild, + NeedsContentChild, NeedsTpl, TextDirective, InertDirective, diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 2a8a60dca6..ee9828f3d2 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -180,6 +180,30 @@ const NG_API = [ 'ComponentUrlMapper', 'ComponentUrlMapper.getUrl', + 'ContentChild', + 'ContentChild.constructor', + 'ContentChild.constructor.constructor', + 'ContentChild.constructor.isVarBindingQuery', + 'ContentChild.constructor.isViewQuery', + 'ContentChild.constructor.selector', + 'ContentChild.constructor.toString', + 'ContentChild.constructor.token', + 'ContentChild.constructor.varBindings', + 'ContentChild.isVarBindingQuery', + 'ContentChild.isViewQuery', + 'ContentChild.selector', + 'ContentChild.toString', + 'ContentChild.token', + 'ContentChild.varBindings', + 'ContentChildMetadata', + 'ContentChildMetadata.constructor', + 'ContentChildMetadata.isVarBindingQuery', + 'ContentChildMetadata.isViewQuery', + 'ContentChildMetadata.selector', + 'ContentChildMetadata.toString', + 'ContentChildMetadata.token', + 'ContentChildMetadata.varBindings', + 'ContentChildren', 'ContentChildren.constructor', 'ContentChildren.constructor.constructor', @@ -860,6 +884,30 @@ const NG_API = [ 'View', + 'ViewChild', + 'ViewChild.constructor', + 'ViewChild.constructor.constructor', + 'ViewChild.constructor.isVarBindingQuery', + 'ViewChild.constructor.isViewQuery', + 'ViewChild.constructor.selector', + 'ViewChild.constructor.toString', + 'ViewChild.constructor.token', + 'ViewChild.constructor.varBindings', + 'ViewChild.isVarBindingQuery', + 'ViewChild.isViewQuery', + 'ViewChild.selector', + 'ViewChild.toString', + 'ViewChild.token', + 'ViewChild.varBindings', + 'ViewChildMetadata', + 'ViewChildMetadata.constructor', + 'ViewChildMetadata.isVarBindingQuery', + 'ViewChildMetadata.isViewQuery', + 'ViewChildMetadata.selector', + 'ViewChildMetadata.toString', + 'ViewChildMetadata.token', + 'ViewChildMetadata.varBindings', + 'ViewChildren', 'ViewChildren.constructor', 'ViewChildren.constructor.constructor',