feat(query): initial implementation of view query.
ViewQuery is a new API that allows a component to query its view. Closes #1935
This commit is contained in:
parent
1eab4f5f07
commit
7ee6963f5d
|
@ -53,5 +53,6 @@ export {
|
||||||
ViewDecorator,
|
ViewDecorator,
|
||||||
ViewFactory,
|
ViewFactory,
|
||||||
Query,
|
Query,
|
||||||
QueryFactory
|
QueryFactory,
|
||||||
|
ViewQuery
|
||||||
} from 'angular2/src/core/annotations/decorators';
|
} from 'angular2/src/core/annotations/decorators';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {ComponentAnnotation, DirectiveAnnotation, LifecycleEvent} from './annotations';
|
import {ComponentAnnotation, DirectiveAnnotation, LifecycleEvent} from './annotations';
|
||||||
import {ViewAnnotation} from './view';
|
import {ViewAnnotation} from './view';
|
||||||
import {AttributeAnnotation, QueryAnnotation} from './di';
|
import {AttributeAnnotation, QueryAnnotation, ViewQueryAnnotation} from './di';
|
||||||
import {
|
import {
|
||||||
makeDecorator,
|
makeDecorator,
|
||||||
makeParamDecorator,
|
makeParamDecorator,
|
||||||
|
@ -368,3 +368,9 @@ export var Attribute: AttributeFactory = makeParamDecorator(AttributeAnnotation)
|
||||||
* {@link Query} factory function.
|
* {@link Query} factory function.
|
||||||
*/
|
*/
|
||||||
export var Query: QueryFactory = makeParamDecorator(QueryAnnotation);
|
export var Query: QueryFactory = makeParamDecorator(QueryAnnotation);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ViewQuery} factory function.
|
||||||
|
*/
|
||||||
|
export var ViewQuery: QueryFactory = makeParamDecorator(ViewQueryAnnotation);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export {
|
export {
|
||||||
Query as QueryAnnotation,
|
Query as QueryAnnotation,
|
||||||
|
ViewQuery as ViewQueryAnnotation,
|
||||||
Attribute as AttributeAnnotation,
|
Attribute as AttributeAnnotation,
|
||||||
} from '../annotations_impl/di';
|
} from '../annotations_impl/di';
|
||||||
|
|
|
@ -57,6 +57,8 @@ export class Query extends DependencyMetadata {
|
||||||
this.descendants = descendants;
|
this.descendants = descendants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isViewQuery() { return false; }
|
||||||
|
|
||||||
get selector() { return resolveForwardRef(this._selector); }
|
get selector() { return resolveForwardRef(this._selector); }
|
||||||
|
|
||||||
get isVarBindingQuery(): boolean { return isString(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)})`; }
|
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)})`; }
|
||||||
|
}
|
||||||
|
|
|
@ -45,4 +45,9 @@ class BaseQueryList<T> extends Object with IterableMixin<T> {
|
||||||
get length => _results.length;
|
get length => _results.length;
|
||||||
get first => _results.first;
|
get first => _results.first;
|
||||||
get last => _results.last;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,6 @@ export class BaseQueryList<T> {
|
||||||
get length() { return this._results.length; }
|
get length() { return this._results.length; }
|
||||||
get first() { return ListWrapper.first(this._results); }
|
get first() { return ListWrapper.first(this._results); }
|
||||||
get last() { return ListWrapper.last(this._results); }
|
get last() { return ListWrapper.last(this._results); }
|
||||||
|
|
||||||
|
map<U>(fn: (T) => U): U[] { return this._results.map(fn); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -434,7 +434,7 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
|
||||||
private _preBuiltObjects = null;
|
private _preBuiltObjects = null;
|
||||||
|
|
||||||
// Queries are added during construction or linking with a new parent.
|
// 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 _query0: QueryRef;
|
||||||
private _query1: QueryRef;
|
private _query1: QueryRef;
|
||||||
private _query2: QueryRef;
|
private _query2: QueryRef;
|
||||||
|
@ -487,6 +487,10 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
|
||||||
|
|
||||||
this._hydrateInjector(imperativelyCreatedInjector, host);
|
this._hydrateInjector(imperativelyCreatedInjector, host);
|
||||||
|
|
||||||
|
if (isPresent(host)) {
|
||||||
|
this._addViewQueries(host);
|
||||||
|
}
|
||||||
|
|
||||||
this._addDirectivesToQueries();
|
this._addDirectivesToQueries();
|
||||||
this._addVarBindingsToQueries();
|
this._addVarBindingsToQueries();
|
||||||
|
|
||||||
|
@ -650,6 +654,22 @@ export class ElementInjector extends TreeNode<ElementInjector> 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 {
|
private _addVarBindingsToQueries(): void {
|
||||||
this._addVarBindingsToQuery(this._query0);
|
this._addVarBindingsToQuery(this._query0);
|
||||||
this._addVarBindingsToQuery(this._query1);
|
this._addVarBindingsToQuery(this._query1);
|
||||||
|
@ -733,15 +753,15 @@ export class ElementInjector extends TreeNode<ElementInjector> implements Depend
|
||||||
|
|
||||||
private _addParentQueries(): void {
|
private _addParentQueries(): void {
|
||||||
if (isBlank(this.parent)) return;
|
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);
|
this._addQueryToTree(this.parent._query0);
|
||||||
if (this.hydrated) this.parent._query0.update();
|
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);
|
this._addQueryToTree(this.parent._query1);
|
||||||
if (this.hydrated) this.parent._query1.update();
|
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);
|
this._addQueryToTree(this.parent._query2);
|
||||||
if (this.hydrated) this.parent._query2.update();
|
if (this.hydrated) this.parent._query2.update();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,10 @@ import {
|
||||||
|
|
||||||
import {Injectable, Optional} from 'angular2/di';
|
import {Injectable, Optional} from 'angular2/di';
|
||||||
import {QueryList} from 'angular2/core';
|
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 {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';
|
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 = '<needs-view-query #q><div text="ignoreme"></div></needs-view-query>';
|
||||||
|
|
||||||
|
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 = '<needs-view-query-desc #q></needs-view-query-desc>';
|
||||||
|
|
||||||
|
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 = '<needs-view-query #q text="self"></needs-view-query>';
|
||||||
|
|
||||||
|
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 = '<needs-view-query-if #q></needs-view-query-if>';
|
||||||
|
|
||||||
|
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 = '<needs-view-query-order #q text="self"></needs-view-query-order>';
|
||||||
|
|
||||||
|
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: '<div text="1"><div text="need descendants"></div></div>' +
|
||||||
|
'<div text="2"></div><div text="3"></div>'
|
||||||
|
})
|
||||||
|
@Injectable()
|
||||||
|
class NeedsViewQuery {
|
||||||
|
query: QueryList<TextDirective>;
|
||||||
|
constructor(@ViewQuery(TextDirective) query: QueryList<TextDirective>) { this.query = query; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'needs-view-query-desc'})
|
||||||
|
@View({
|
||||||
|
directives: [TextDirective],
|
||||||
|
template: '<div text="1"><div text="2"></div></div>' +
|
||||||
|
'<div text="3"></div><div text="4"></div>'
|
||||||
|
})
|
||||||
|
@Injectable()
|
||||||
|
class NeedsViewQueryDesc {
|
||||||
|
query: QueryList<TextDirective>;
|
||||||
|
constructor(@ViewQuery(TextDirective, {descendants: true}) query: QueryList<TextDirective>) {
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'needs-view-query-if'})
|
||||||
|
@View({directives: [NgIf, TextDirective], template: '<div *ng-if="show" text="1"></div>'})
|
||||||
|
@Injectable()
|
||||||
|
class NeedsViewQueryIf {
|
||||||
|
show: boolean;
|
||||||
|
query: QueryList<TextDirective>;
|
||||||
|
constructor(@ViewQuery(TextDirective) query: QueryList<TextDirective>) {
|
||||||
|
this.query = query;
|
||||||
|
this.show = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({selector: 'needs-view-query-order'})
|
||||||
|
@View({
|
||||||
|
directives: [NgFor, TextDirective],
|
||||||
|
template: '<div text="1">' +
|
||||||
|
'<div *ng-for="var i of [\'2\', \'3\']" [text]="i"></div>' +
|
||||||
|
'<div text="4"'
|
||||||
|
})
|
||||||
|
@Injectable()
|
||||||
|
class NeedsViewQueryOrder {
|
||||||
|
query: QueryList<TextDirective>;
|
||||||
|
constructor(@ViewQuery(TextDirective) query: QueryList<TextDirective>) { this.query = query; }
|
||||||
|
}
|
||||||
|
|
||||||
@Component({selector: 'my-comp'})
|
@Component({selector: 'my-comp'})
|
||||||
@View({
|
@View({
|
||||||
directives: [
|
directives: [
|
||||||
|
@ -328,6 +476,10 @@ class NeedsQueryByTwoLabels {
|
||||||
NeedsQueryDesc,
|
NeedsQueryDesc,
|
||||||
NeedsQueryByLabel,
|
NeedsQueryByLabel,
|
||||||
NeedsQueryByTwoLabels,
|
NeedsQueryByTwoLabels,
|
||||||
|
NeedsViewQuery,
|
||||||
|
NeedsViewQueryDesc,
|
||||||
|
NeedsViewQueryIf,
|
||||||
|
NeedsViewQueryOrder,
|
||||||
TextDirective,
|
TextDirective,
|
||||||
NgIf,
|
NgIf,
|
||||||
NgFor
|
NgFor
|
||||||
|
|
|
@ -6,9 +6,10 @@ import {QueryList} from 'angular2/src/core/compiler/query_list';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('QueryList', () => {
|
describe('QueryList', () => {
|
||||||
var queryList, log;
|
var queryList: QueryList<string>;
|
||||||
|
var log: string;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
queryList = new QueryList();
|
queryList = new QueryList<string>();
|
||||||
log = '';
|
log = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -36,6 +37,12 @@ export function main() {
|
||||||
expect(queryList.length).toEqual(2);
|
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', () => {
|
it('should support first and last', () => {
|
||||||
queryList.add('one');
|
queryList.add('one');
|
||||||
queryList.add('two');
|
queryList.add('two');
|
||||||
|
|
Loading…
Reference in New Issue