feat(core): add support for @ContentChild and @ViewChild

Closes #4251
This commit is contained in:
vsavkin 2015-09-19 18:39:35 -07:00 committed by Victor Savkin
parent 2e9de0b169
commit c2a60f1624
8 changed files with 313 additions and 23 deletions

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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.
*/

View File

@ -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);

View File

@ -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}); }
}

View File

@ -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")});
});
});
});
}

View File

@ -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 =
'<needs-content-child #q><div *ng-if="shouldShow" text="foo"></div></needs-content-child>';
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 = '<needs-view-child #q></needs-view-child>';
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 = '<div text="1"></div>' +
@ -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<TextDirective>;
@ContentChildren(TextDirective) textDirChildren: QueryList<TextDirective>;
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: '<div text></div>', directives: [TextDirective]})
class NeedsViewChildren implements AfterViewInit {
textDirChildren: QueryList<TextDirective>;
@ViewChildren(TextDirective) textDirChildren: QueryList<TextDirective>;
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: `
<div *ng-if="shouldShow" text="foo"></div>
`,
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,

View File

@ -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',