feat(ivy): support deep queries through view boundaries (#21700)

PR Close #21700
This commit is contained in:
Pawel Kozlowski 2018-01-17 17:55:55 +01:00 committed by Misko Hevery
parent 3e03dbe576
commit 5269ce287e
9 changed files with 519 additions and 82 deletions

View File

@ -8,14 +8,8 @@
import './ng_dev_mode'; import './ng_dev_mode';
import {ElementRef} from '../linker/element_ref';
import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref';
import {Type} from '../type';
import {assertEqual, assertLessThan, assertNotEqual, assertNotNull} from './assert'; import {assertEqual, assertLessThan, assertNotEqual, assertNotNull} from './assert';
import {LContainer, TContainer} from './interfaces/container'; import {LContainer, TContainer} from './interfaces/container';
import {LInjector} from './interfaces/injector';
import {CssSelector, LProjection} from './interfaces/projection'; import {CssSelector, LProjection} from './interfaces/projection';
import {LQuery, QueryReadType} from './interfaces/query'; import {LQuery, QueryReadType} from './interfaces/query';
import {LView, LifecycleStage, TData, TView} from './interfaces/view'; import {LView, LifecycleStage, TData, TView} from './interfaces/view';
@ -147,6 +141,8 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | null)
} }
currentView = newView; currentView = newView;
currentQuery = newView.query;
return oldView !; return oldView !;
} }
@ -181,7 +177,8 @@ export function createLView(
template: template, template: template,
context: context, context: context,
dynamicViewCount: 0, dynamicViewCount: 0,
lifecycleStage: LifecycleStage.INIT lifecycleStage: LifecycleStage.INIT,
query: null,
}; };
return newView; return newView;
@ -1003,14 +1000,17 @@ export function container(
renderParent = currentParent as LElementNode; renderParent = currentParent as LElementNode;
} }
const node = createLNode(index, LNodeFlags.Container, comment, <LContainer>{ const lContainer = <LContainer>{
views: [], views: [],
nextIndex: 0, renderParent, nextIndex: 0, renderParent,
template: template == null ? null : template, template: template == null ? null : template,
next: null, next: null,
parent: currentView, parent: currentView,
dynamicViewCount: 0, dynamicViewCount: 0,
}); query: null
};
const node = createLNode(index, LNodeFlags.Container, comment, lContainer);
if (node.tNode == null) { if (node.tNode == null) {
// TODO(misko): implement queryName caching // TODO(misko): implement queryName caching
@ -1025,8 +1025,13 @@ export function container(
isParent = false; isParent = false;
ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container); ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container);
const query = previousOrParentNode.query; const query = node.query;
query && query.addNode(previousOrParentNode); if (query) {
// check if a given container node matches
query.addNode(node);
// prepare place for matching nodes from views inserted into a given container
lContainer.query = query.container();
}
} }
/** /**
@ -1107,6 +1112,10 @@ export function viewStart(viewBlockId: number): boolean {
// When we create a new LView, we always reset the state of the instructions. // When we create a new LView, we always reset the state of the instructions.
const newView = const newView =
createLView(viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container)); createLView(viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container));
if (lContainer.query) {
newView.query = lContainer.query.enterView(lContainer.nextIndex);
}
enterView(newView, createLNode(null, LNodeFlags.View, null, newView)); enterView(newView, createLNode(null, LNodeFlags.View, null, newView));
lContainer.nextIndex++; lContainer.nextIndex++;
} }

View File

@ -8,9 +8,11 @@
import {ComponentTemplate} from './definition'; import {ComponentTemplate} from './definition';
import {LElementNode, LViewNode} from './node'; import {LElementNode, LViewNode} from './node';
import {LQuery} from './query';
import {LView, TView} from './view'; import {LView, TView} from './view';
/** The state associated with an LContainer */ /** The state associated with an LContainer */
export interface LContainer { export interface LContainer {
/** /**
@ -67,12 +69,17 @@ export interface LContainer {
*/ */
readonly template: ComponentTemplate<any>|null; readonly template: ComponentTemplate<any>|null;
/** /**
* A count of dynamic views rendered into this container. If this is non-zero, the `views` array * A count of dynamic views rendered into this container. If this is non-zero, the `views` array
* will be traversed when refreshing dynamic views on this container. * will be traversed when refreshing dynamic views on this container.
*/ */
dynamicViewCount: number; dynamicViewCount: number;
/**
* Queries active for this container - all the views inserted to / removed from
* this container are reported to queries referenced here.
*/
query: LQuery|null;
} }
/** /**

View File

@ -8,9 +8,7 @@
import {QueryList} from '../../linker'; import {QueryList} from '../../linker';
import {Type} from '../../type'; import {Type} from '../../type';
import {LNode} from './node';
import {LInjector} from './injector';
import {LContainerNode, LNode, LViewNode} from './node';
/** Used for tracking queries (e.g. ViewChild, ContentChild). */ /** Used for tracking queries (e.g. ViewChild, ContentChild). */
@ -25,19 +23,28 @@ export interface LQuery {
child(): LQuery|null; child(): LQuery|null;
/** /**
* Notify `LQuery` that a `LNode` has been created. * Notify `LQuery` that a new `LNode` has been created and needs to be added to query results
* if matching query predicate.
*/ */
addNode(node: LNode): void; addNode(node: LNode): void;
/** /**
* Notify `LQuery` that an `LViewNode` has been added to `LContainerNode`. * Notify `LQuery` that a `LNode` has been created and needs to be added to query results
* if matching query predicate.
*/ */
insertView(container: LContainerNode, view: LViewNode, insertIndex: number): void; container(): LQuery|null;
/** /**
* Notify `LQuery` that an `LViewNode` has been removed from `LContainerNode`. * Notify `LQuery` that a new view was created and is being entered in the creation mode.
* This allow queries to prepare space for matching nodes from views.
*/ */
removeView(container: LContainerNode, view: LViewNode, removeIndex: number): void; enterView(newViewIndex: number): LQuery|null;
/**
* Notify `LQuery` that an `LViewNode` has been removed from `LContainerNode`. As a result all
* the matching nodes from this view should be removed from container's queries.
*/
removeView(removeIndex: number): void;
/** /**
* Add additional `QueryList` to track. * Add additional `QueryList` to track.

View File

@ -9,6 +9,7 @@
import {LContainer} from './container'; import {LContainer} from './container';
import {ComponentTemplate, DirectiveDef} from './definition'; import {ComponentTemplate, DirectiveDef} from './definition';
import {LElementNode, LViewNode, TNode} from './node'; import {LElementNode, LViewNode, TNode} from './node';
import {LQuery} from './query';
import {Renderer3} from './renderer'; import {Renderer3} from './renderer';
@ -170,6 +171,11 @@ export interface LView {
* after refreshing the view itself. * after refreshing the view itself.
*/ */
dynamicViewCount: number; dynamicViewCount: number;
/**
* Queries active for this view - nodes from a view are reported to those queries
*/
query: LQuery|null;
} }
/** Interface necessary to work with view tree traversal */ /** Interface necessary to work with view tree traversal */

View File

@ -222,8 +222,6 @@ export function insertView(
container, newView, true, findBeforeNode(index, state, container.native)); container, newView, true, findBeforeNode(index, state, container.native));
} }
// Notify query that view has been inserted
container.query && container.query.insertView(container, newView, index);
return newView; return newView;
} }
@ -248,7 +246,7 @@ export function removeView(container: LContainerNode, removeIndex: number): LVie
destroyViewTree(viewNode.data); destroyViewTree(viewNode.data);
addRemoveViewFromContainer(container, viewNode, false); addRemoveViewFromContainer(container, viewNode, false);
// Notify query that view has been removed // Notify query that view has been removed
container.query && container.query.removeView(container, viewNode, removeIndex); container.data.query && container.data.query.removeView(removeIndex);
return viewNode; return viewNode;
} }

View File

@ -10,37 +10,21 @@
// correctly implementing its interfaces for backwards compatibility. // correctly implementing its interfaces for backwards compatibility.
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {ElementRef as viewEngine_ElementRef} from '../linker/element_ref';
import {QueryList as viewEngine_QueryList} from '../linker/query_list'; import {QueryList as viewEngine_QueryList} from '../linker/query_list';
import {TemplateRef as viewEngine_TemplateRef} from '../linker/template_ref';
import {Type} from '../type'; import {Type} from '../type';
import {assertNotNull} from './assert'; import {assertEqual, assertNotNull} from './assert';
import {ReadFromInjectorFn, getOrCreateNodeInjectorForNode} from './di'; import {ReadFromInjectorFn, getOrCreateNodeInjectorForNode} from './di';
import {assertPreviousIsParent, getCurrentQuery} from './instructions'; import {assertPreviousIsParent, getCurrentQuery} from './instructions';
import {DirectiveDef, unusedValueExportToPlacateAjd as unused1} from './interfaces/definition'; import {DirectiveDef, unusedValueExportToPlacateAjd as unused1} from './interfaces/definition';
import {LInjector, unusedValueExportToPlacateAjd as unused2} from './interfaces/injector'; import {LInjector, unusedValueExportToPlacateAjd as unused2} from './interfaces/injector';
import {LContainerNode, LElementNode, LNode, LNodeFlags, LViewNode, TNode, unusedValueExportToPlacateAjd as unused3} from './interfaces/node'; import {LContainerNode, LElementNode, LNode, LNodeFlags, TNode, unusedValueExportToPlacateAjd as unused3} from './interfaces/node';
import {LQuery, QueryReadType, unusedValueExportToPlacateAjd as unused4} from './interfaces/query'; import {LQuery, QueryReadType, unusedValueExportToPlacateAjd as unused4} from './interfaces/query';
import {assertNodeOfPossibleTypes} from './node_assert'; import {flatten} from './util';
const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4;
export function query<T>(
predicate: Type<any>| string[], descend?: boolean,
read?: QueryReadType<T>| Type<T>): QueryList<T> {
ngDevMode && assertPreviousIsParent();
const queryList = new QueryList<T>();
const query = getCurrentQuery(LQuery_);
query.track(queryList, predicate, descend, read);
return queryList;
}
export function queryRefresh(query: QueryList<any>): boolean {
return (query as any)._refresh();
}
/** /**
* A predicate which determines if a given element/directive should be included in the query * A predicate which determines if a given element/directive should be included in the query
*/ */
@ -112,17 +96,90 @@ export class LQuery_ implements LQuery {
} }
} }
container(): LQuery|null {
let result: QueryPredicate<any>|null = null;
let predicate = this.deep;
while (predicate) {
const containerValues: any[] = []; // prepare room for views
predicate.values.push(containerValues);
const clonedPredicate: QueryPredicate<any> = {
next: null,
list: predicate.list,
type: predicate.type,
selector: predicate.selector,
read: predicate.read,
values: containerValues
};
clonedPredicate.next = result;
result = clonedPredicate;
predicate = predicate.next;
}
return result ? new LQuery_(result) : null;
}
enterView(index: number): LQuery|null {
let result: QueryPredicate<any>|null = null;
let predicate = this.deep;
while (predicate) {
const viewValues: any[] = []; // prepare room for view nodes
predicate.values.splice(index, 0, viewValues);
const clonedPredicate: QueryPredicate<any> = {
next: null,
list: predicate.list,
type: predicate.type,
selector: predicate.selector,
read: predicate.read,
values: viewValues
};
clonedPredicate.next = result;
result = clonedPredicate;
predicate = predicate.next;
}
return result ? new LQuery_(result) : null;
}
addNode(node: LNode): void { addNode(node: LNode): void {
add(this.shallow, node); add(this.shallow, node);
add(this.deep, node); add(this.deep, node);
} }
insertView(container: LContainerNode, view: LViewNode, index: number): void { removeView(index: number): void {
throw new Error('Method not implemented.'); let predicate = this.deep;
while (predicate) {
const removed = predicate.values.splice(index, 1);
// mark a query as dirty only when removed view had matching modes
ngDevMode && assertEqual(removed.length, 1, 'removed.length');
if (removed[0].length) {
predicate.list.setDirty();
} }
removeView(container: LContainerNode, view: LViewNode, index: number): void { predicate = predicate.next;
throw new Error('Method not implemented.'); }
}
/**
* Clone LQuery by taking all the deep query predicates and cloning those using a provided clone
* function.
* Shallow predicates are ignored.
*/
private _clonePredicates(
predicateCloneFn: (predicate: QueryPredicate<any>) => QueryPredicate<any>): LQuery|null {
let result: QueryPredicate<any>|null = null;
let predicate = this.deep;
while (predicate) {
const clonedPredicate = predicateCloneFn(predicate);
clonedPredicate.next = result;
result = clonedPredicate;
predicate = predicate.next;
}
return result ? new LQuery_(result) : null;
} }
} }
@ -191,10 +248,10 @@ function add(predicate: QueryPredicate<any>| null, node: LNode) {
if (predicate.read !== null) { if (predicate.read !== null) {
const requestedRead = readFromNodeInjector(nodeInjector, node, predicate.read); const requestedRead = readFromNodeInjector(nodeInjector, node, predicate.read);
if (requestedRead !== null) { if (requestedRead !== null) {
predicate.values.push(requestedRead); addMatch(predicate, requestedRead);
} }
} else { } else {
predicate.values.push(node.view.data[directiveIdx]); addMatch(predicate, node.view.data[directiveIdx]);
} }
} }
} else { } else {
@ -207,10 +264,10 @@ function add(predicate: QueryPredicate<any>| null, node: LNode) {
if (predicate.read !== null) { if (predicate.read !== null) {
const result = readFromNodeInjector(nodeInjector, node, predicate.read !, directiveIdx); const result = readFromNodeInjector(nodeInjector, node, predicate.read !, directiveIdx);
if (result !== null) { if (result !== null) {
predicate.values.push(result); addMatch(predicate, result);
} }
} else { } else {
predicate.values.push(node.view.data[directiveIdx]); addMatch(predicate, node.view.data[directiveIdx]);
} }
} }
} }
@ -219,27 +276,31 @@ function add(predicate: QueryPredicate<any>| null, node: LNode) {
} }
} }
function addMatch(predicate: QueryPredicate<any>, matchingValue: any): void {
predicate.values.push(matchingValue);
predicate.list.setDirty();
}
function createPredicate<T>( function createPredicate<T>(
previous: QueryPredicate<any>| null, queryList: QueryList<T>, predicate: Type<T>| string[], previous: QueryPredicate<any>| null, queryList: QueryList<T>, predicate: Type<T>| string[],
read: QueryReadType<T>| Type<T>| null): QueryPredicate<T> { read: QueryReadType<T>| Type<T>| null): QueryPredicate<T> {
const isArray = Array.isArray(predicate); const isArray = Array.isArray(predicate);
const values = <any>[];
if ((queryList as any as QueryList_<T>)._valuesTree === null) {
(queryList as any as QueryList_<T>)._valuesTree = values;
}
return { return {
next: previous, next: previous,
list: queryList, list: queryList,
type: isArray ? null : predicate as Type<T>, type: isArray ? null : predicate as Type<T>,
selector: isArray ? predicate as string[] : null, selector: isArray ? predicate as string[] : null,
read: read, read: read,
values: values values: (queryList as any as QueryList_<T>)._valuesTree
}; };
} }
class QueryList_<T>/* implements viewEngine_QueryList<T> */ { class QueryList_<T>/* implements viewEngine_QueryList<T> */ {
dirty: boolean = false; readonly dirty = true;
changes: Observable<T>; readonly changes: Observable<T>;
private _values: T[]|null = null;
/** @internal */
_valuesTree: any[] = [];
get length(): number { get length(): number {
ngDevMode && assertNotNull(this._values, 'refreshed'); ngDevMode && assertNotNull(this._values, 'refreshed');
@ -258,21 +319,6 @@ class QueryList_<T>/* implements viewEngine_QueryList<T> */ {
return values.length ? values[values.length - 1] : null; return values.length ? values[values.length - 1] : null;
} }
/** @internal */
_valuesTree: any[]|null = null;
/** @internal */
_values: T[]|null = null;
/** @internal */
_refresh(): boolean {
// TODO(misko): needs more logic to flatten tree.
if (this._values === null) {
this._values = this._valuesTree;
return true;
}
return false;
}
map<U>(fn: (item: T, index: number, array: T[]) => U): U[] { map<U>(fn: (item: T, index: number, array: T[]) => U): U[] {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@ -295,10 +341,16 @@ class QueryList_<T>/* implements viewEngine_QueryList<T> */ {
ngDevMode && assertNotNull(this._values, 'refreshed'); ngDevMode && assertNotNull(this._values, 'refreshed');
return this._values !; return this._values !;
} }
toString(): string { throw new Error('Method not implemented.'); } toString(): string {
reset(res: (any[]|T)[]): void { throw new Error('Method not implemented.'); } ngDevMode && assertNotNull(this._values, 'refreshed');
return this._values !.toString();
}
reset(res: (any[]|T)[]): void {
this._values = flatten(res);
(this as{dirty: boolean}).dirty = false;
}
notifyOnChanges(): void { throw new Error('Method not implemented.'); } notifyOnChanges(): void { throw new Error('Method not implemented.'); }
setDirty(): void { throw new Error('Method not implemented.'); } setDirty(): void { (this as{dirty: boolean}).dirty = true; }
destroy(): void { throw new Error('Method not implemented.'); } destroy(): void { throw new Error('Method not implemented.'); }
} }
@ -306,3 +358,27 @@ class QueryList_<T>/* implements viewEngine_QueryList<T> */ {
// it can't be implemented only extended. // it can't be implemented only extended.
export type QueryList<T> = viewEngine_QueryList<T>; export type QueryList<T> = viewEngine_QueryList<T>;
export const QueryList: typeof viewEngine_QueryList = QueryList_ as any; export const QueryList: typeof viewEngine_QueryList = QueryList_ as any;
export function query<T>(
predicate: Type<any>| string[], descend?: boolean,
read?: QueryReadType<T>| Type<T>): QueryList<T> {
ngDevMode && assertPreviousIsParent();
const queryList = new QueryList<T>();
const query = getCurrentQuery(LQuery_);
query.track(queryList, predicate, descend, read);
return queryList;
}
/**
* Refreshes a query by combining matches from all active views and removing matches from deleted
* views.
* Returns true if a query got dirty during change detection, false otherwise.
*/
export function queryRefresh(query: QueryList<any>): boolean {
const queryImpl = (query as any as QueryList_<any>);
if (query.dirty) {
query.reset(queryImpl._valuesTree);
return true;
}
return false;
}

View File

@ -31,3 +31,28 @@ export function stringify(value: any): string {
export function notImplemented(): Error { export function notImplemented(): Error {
return new Error('NotImplemented'); return new Error('NotImplemented');
} }
/**
* Flattens an array in non-recursive way. Input arrays are not modified.
*/
export function flatten(list: any[]): any[] {
const result: any[] = [];
let i = 0;
while (i < list.length) {
const item = list[i];
if (Array.isArray(item)) {
if (item.length > 0) {
list = item.concat(list.slice(i + 1));
i = 0;
} else {
i++;
}
} else {
result.push(item);
i++;
}
}
return result;
}

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF} from '../../src/render3/di'; import {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF} from '../../src/render3/di';
import {C, E, Q, QueryList, e, m, qR} from '../../src/render3/index'; import {C, E, Q, QueryList, V, cR, cr, detectChanges, e, m, qR, v} from '../../src/render3/index';
import {QueryReadType} from '../../src/render3/interfaces/query';
import {createComponent, createDirective, renderComponent} from './render_util'; import {createComponent, createDirective, renderComponent} from './render_util';
@ -511,7 +510,6 @@ describe('query', () => {
it('should not add results to query if a requested token cant be read', () => { it('should not add results to query if a requested token cant be read', () => {
const Child = createDirective(); const Child = createDirective();
let childInstance, div;
/** /**
* <div #foo></div> * <div #foo></div>
* class Cmpt { * class Cmpt {
@ -522,7 +520,7 @@ describe('query', () => {
let tmp: any; let tmp: any;
if (cm) { if (cm) {
m(0, Q(['foo'], false, Child)); m(0, Q(['foo'], false, Child));
div = E(1, 'div', null, null, ['foo', '']); E(1, 'div', null, null, ['foo', '']);
e(); e();
} }
qR(tmp = m<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>); qR(tmp = m<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>);
@ -534,4 +532,299 @@ describe('query', () => {
}); });
}); });
describe('view boundaries', () => {
it('should report results in embedded views', () => {
let firstEl;
/**
* <ng-template [ngIf]="exp">
* <div #foo></div>
* </ng-template>
* class Cmpt {
* @ViewChildren('foo') query;
* }
*/
const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) {
let tmp: any;
if (cm) {
m(0, Q(['foo'], true, QUERY_READ_FROM_NODE));
C(1);
}
cR(1);
{
if (ctx.exp) {
let cm1 = V(1);
{
if (cm1) {
firstEl = E(0, 'div', null, null, ['foo', '']);
e();
}
}
v();
}
}
cr();
qR(tmp = m<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>);
});
const cmptInstance = renderComponent(Cmpt);
const query = (cmptInstance.query as any);
expect(query.length).toBe(0);
cmptInstance.exp = true;
detectChanges(cmptInstance);
expect(query.length).toBe(1);
expect(query.first.nativeElement).toBe(firstEl);
cmptInstance.exp = false;
detectChanges(cmptInstance);
expect(query.length).toBe(0);
});
it('should add results from embedded views in the correct order - views and elements mix',
() => {
let firstEl, lastEl, viewEl;
/**
* <span #foo></span>
* <ng-template [ngIf]="exp">
* <div #foo></div>
* </ng-template>
* <span #foo></span>
* class Cmpt {
* @ViewChildren('foo') query;
* }
*/
const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) {
let tmp: any;
if (cm) {
m(0, Q(['foo'], true, QUERY_READ_FROM_NODE));
firstEl = E(1, 'b', null, null, ['foo', '']);
e();
C(2);
lastEl = E(3, 'i', null, null, ['foo', '']);
e();
}
cR(2);
{
if (ctx.exp) {
let cm1 = V(1);
{
if (cm1) {
viewEl = E(0, 'div', null, null, ['foo', '']);
e();
}
}
v();
}
}
cr();
qR(tmp = m<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>);
});
const cmptInstance = renderComponent(Cmpt);
const query = (cmptInstance.query as any);
expect(query.length).toBe(2);
expect(query.first.nativeElement).toBe(firstEl);
expect(query.last.nativeElement).toBe(lastEl);
cmptInstance.exp = true;
detectChanges(cmptInstance);
expect(query.length).toBe(3);
expect(query.toArray()[0].nativeElement).toBe(firstEl);
expect(query.toArray()[1].nativeElement).toBe(viewEl);
expect(query.toArray()[2].nativeElement).toBe(lastEl);
cmptInstance.exp = false;
detectChanges(cmptInstance);
expect(query.length).toBe(2);
expect(query.first.nativeElement).toBe(firstEl);
expect(query.last.nativeElement).toBe(lastEl);
});
it('should add results from embedded views in the correct order - views side by side', () => {
let firstEl, lastEl;
/**
* <ng-template [ngIf]="exp1">
* <div #foo></div>
* </ng-template>
* <ng-template [ngIf]="exp2">
* <span #foo></span>
* </ng-template>
* class Cmpt {
* @ViewChildren('foo') query;
* }
*/
const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) {
let tmp: any;
if (cm) {
m(0, Q(['foo'], true, QUERY_READ_FROM_NODE));
C(1);
}
cR(1);
{
if (ctx.exp1) {
let cm1 = V(0);
{
if (cm1) {
firstEl = E(0, 'div', null, null, ['foo', '']);
e();
}
}
v();
}
if (ctx.exp2) {
let cm1 = V(1);
{
if (cm1) {
lastEl = E(0, 'span', null, null, ['foo', '']);
e();
}
}
v();
}
}
cr();
qR(tmp = m<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>);
});
const cmptInstance = renderComponent(Cmpt);
const query = (cmptInstance.query as any);
expect(query.length).toBe(0);
cmptInstance.exp2 = true;
detectChanges(cmptInstance);
expect(query.length).toBe(1);
expect(query.last.nativeElement).toBe(lastEl);
cmptInstance.exp1 = true;
detectChanges(cmptInstance);
expect(query.length).toBe(2);
expect(query.first.nativeElement).toBe(firstEl);
expect(query.last.nativeElement).toBe(lastEl);
});
it('should add results from embedded views in the correct order - nested views', () => {
let firstEl, lastEl;
/**
* <ng-template [ngIf]="exp1">
* <div #foo></div>
* <ng-template [ngIf]="exp2">
* <span #foo></span>
* </ng-template>
* </ng-template>
* class Cmpt {
* @ViewChildren('foo') query;
* }
*/
const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) {
let tmp: any;
if (cm) {
m(0, Q(['foo'], true, QUERY_READ_FROM_NODE));
C(1);
}
cR(1);
{
if (ctx.exp1) {
let cm1 = V(0);
{
if (cm1) {
firstEl = E(0, 'div', null, null, ['foo', '']);
e();
C(1);
}
cR(1);
{
if (ctx.exp2) {
let cm2 = V(0);
{
if (cm2) {
lastEl = E(0, 'span', null, null, ['foo', '']);
e();
}
}
v();
}
}
cr();
}
v();
}
}
cr();
qR(tmp = m<QueryList<any>>(0)) && (ctx.query = tmp as QueryList<any>);
});
const cmptInstance = renderComponent(Cmpt);
const query = (cmptInstance.query as any);
expect(query.length).toBe(0);
cmptInstance.exp1 = true;
detectChanges(cmptInstance);
expect(query.length).toBe(1);
expect(query.first.nativeElement).toBe(firstEl);
cmptInstance.exp2 = true;
detectChanges(cmptInstance);
expect(query.length).toBe(2);
expect(query.first.nativeElement).toBe(firstEl);
expect(query.last.nativeElement).toBe(lastEl);
});
it('should support combination of deep and shallow queries', () => {
/**
* <ng-template [ngIf]="exp">
* <div #foo></div>
* </ng-template>
* <span #foo></span>
* class Cmpt {
* @ViewChildren('foo') query;
* }
*/
const Cmpt = createComponent('cmpt', function(ctx: any, cm: boolean) {
let tmp: any;
if (cm) {
m(0, Q(['foo'], true));
m(1, Q(['foo'], false));
C(2);
E(3, 'span', null, null, ['foo', '']);
e();
}
cR(2);
{
if (ctx.exp) {
let cm1 = V(0);
{
if (cm1) {
E(0, 'div', null, null, ['foo', '']);
e();
}
}
v();
}
}
cr();
qR(tmp = m<QueryList<any>>(0)) && (ctx.deep = tmp as QueryList<any>);
qR(tmp = m<QueryList<any>>(1)) && (ctx.shallow = tmp as QueryList<any>);
});
const cmptInstance = renderComponent(Cmpt);
const deep = (cmptInstance.deep as any);
const shallow = (cmptInstance.shallow as any);
expect(deep.length).toBe(1);
expect(shallow.length).toBe(1);
cmptInstance.exp = true;
detectChanges(cmptInstance);
expect(deep.length).toBe(2);
expect(shallow.length).toBe(1);
cmptInstance.exp = false;
detectChanges(cmptInstance);
expect(deep.length).toBe(1);
expect(shallow.length).toBe(1);
});
});
}); });

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {isDifferent} from '../../src/render3/util'; import {flatten, isDifferent} from '../../src/render3/util';
describe('util', () => { describe('util', () => {
@ -32,4 +32,20 @@ describe('util', () => {
expect(isDifferent(5, NaN)).toBeTruthy(); expect(isDifferent(5, NaN)).toBeTruthy();
}); });
}); });
describe('flatten', () => {
it('should flatten an empty array', () => { expect(flatten([])).toEqual([]); });
it('should flatten a flat array', () => { expect(flatten([1, 2, 3])).toEqual([1, 2, 3]); });
it('should flatten a nested array', () => {
expect(flatten([1, [2], 3])).toEqual([1, 2, 3]);
expect(flatten([[1], 2, [3]])).toEqual([1, 2, 3]);
expect(flatten([1, [2, [3]], 4])).toEqual([1, 2, 3, 4]);
expect(flatten([1, [2, [3]], [4]])).toEqual([1, 2, 3, 4]);
expect(flatten([1, [2, [3]], [[[4]]]])).toEqual([1, 2, 3, 4]);
expect(flatten([1, [], 2])).toEqual([1, 2]);
});
});
}); });