feat(ivy): support deep queries through view boundaries (#21700)
PR Close #21700
This commit is contained in:
parent
3e03dbe576
commit
5269ce287e
|
@ -8,14 +8,8 @@
|
|||
|
||||
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 {LContainer, TContainer} from './interfaces/container';
|
||||
import {LInjector} from './interfaces/injector';
|
||||
import {CssSelector, LProjection} from './interfaces/projection';
|
||||
import {LQuery, QueryReadType} from './interfaces/query';
|
||||
import {LView, LifecycleStage, TData, TView} from './interfaces/view';
|
||||
|
@ -147,6 +141,8 @@ export function enterView(newView: LView, host: LElementNode | LViewNode | null)
|
|||
}
|
||||
|
||||
currentView = newView;
|
||||
currentQuery = newView.query;
|
||||
|
||||
return oldView !;
|
||||
}
|
||||
|
||||
|
@ -181,7 +177,8 @@ export function createLView(
|
|||
template: template,
|
||||
context: context,
|
||||
dynamicViewCount: 0,
|
||||
lifecycleStage: LifecycleStage.INIT
|
||||
lifecycleStage: LifecycleStage.INIT,
|
||||
query: null,
|
||||
};
|
||||
|
||||
return newView;
|
||||
|
@ -1003,14 +1000,17 @@ export function container(
|
|||
renderParent = currentParent as LElementNode;
|
||||
}
|
||||
|
||||
const node = createLNode(index, LNodeFlags.Container, comment, <LContainer>{
|
||||
const lContainer = <LContainer>{
|
||||
views: [],
|
||||
nextIndex: 0, renderParent,
|
||||
template: template == null ? null : template,
|
||||
next: null,
|
||||
parent: currentView,
|
||||
dynamicViewCount: 0,
|
||||
});
|
||||
query: null
|
||||
};
|
||||
|
||||
const node = createLNode(index, LNodeFlags.Container, comment, lContainer);
|
||||
|
||||
if (node.tNode == null) {
|
||||
// TODO(misko): implement queryName caching
|
||||
|
@ -1025,8 +1025,13 @@ export function container(
|
|||
|
||||
isParent = false;
|
||||
ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container);
|
||||
const query = previousOrParentNode.query;
|
||||
query && query.addNode(previousOrParentNode);
|
||||
const query = node.query;
|
||||
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.
|
||||
const newView =
|
||||
createLView(viewBlockId, renderer, getOrCreateEmbeddedTView(viewBlockId, container));
|
||||
if (lContainer.query) {
|
||||
newView.query = lContainer.query.enterView(lContainer.nextIndex);
|
||||
}
|
||||
|
||||
enterView(newView, createLNode(null, LNodeFlags.View, null, newView));
|
||||
lContainer.nextIndex++;
|
||||
}
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
|
||||
import {ComponentTemplate} from './definition';
|
||||
import {LElementNode, LViewNode} from './node';
|
||||
import {LQuery} from './query';
|
||||
import {LView, TView} from './view';
|
||||
|
||||
|
||||
|
||||
/** The state associated with an LContainer */
|
||||
export interface LContainer {
|
||||
/**
|
||||
|
@ -67,12 +69,17 @@ export interface LContainer {
|
|||
*/
|
||||
readonly template: ComponentTemplate<any>|null;
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
|
||||
import {QueryList} from '../../linker';
|
||||
import {Type} from '../../type';
|
||||
|
||||
import {LInjector} from './injector';
|
||||
import {LContainerNode, LNode, LViewNode} from './node';
|
||||
import {LNode} from './node';
|
||||
|
||||
|
||||
/** Used for tracking queries (e.g. ViewChild, ContentChild). */
|
||||
|
@ -25,19 +23,28 @@ export interface LQuery {
|
|||
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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import {LContainer} from './container';
|
||||
import {ComponentTemplate, DirectiveDef} from './definition';
|
||||
import {LElementNode, LViewNode, TNode} from './node';
|
||||
import {LQuery} from './query';
|
||||
import {Renderer3} from './renderer';
|
||||
|
||||
|
||||
|
@ -170,6 +171,11 @@ export interface LView {
|
|||
* after refreshing the view itself.
|
||||
*/
|
||||
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 */
|
||||
|
|
|
@ -222,8 +222,6 @@ export function insertView(
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -248,7 +246,7 @@ export function removeView(container: LContainerNode, removeIndex: number): LVie
|
|||
destroyViewTree(viewNode.data);
|
||||
addRemoveViewFromContainer(container, viewNode, false);
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,37 +10,21 @@
|
|||
// correctly implementing its interfaces for backwards compatibility.
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
import {ElementRef as viewEngine_ElementRef} from '../linker/element_ref';
|
||||
import {QueryList as viewEngine_QueryList} from '../linker/query_list';
|
||||
import {TemplateRef as viewEngine_TemplateRef} from '../linker/template_ref';
|
||||
import {Type} from '../type';
|
||||
|
||||
import {assertNotNull} from './assert';
|
||||
import {assertEqual, assertNotNull} from './assert';
|
||||
import {ReadFromInjectorFn, getOrCreateNodeInjectorForNode} from './di';
|
||||
import {assertPreviousIsParent, getCurrentQuery} from './instructions';
|
||||
import {DirectiveDef, unusedValueExportToPlacateAjd as unused1} from './interfaces/definition';
|
||||
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 {assertNodeOfPossibleTypes} from './node_assert';
|
||||
import {flatten} from './util';
|
||||
|
||||
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
|
||||
*/
|
||||
|
@ -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 {
|
||||
add(this.shallow, node);
|
||||
add(this.deep, node);
|
||||
}
|
||||
|
||||
insertView(container: LContainerNode, view: LViewNode, index: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
removeView(index: number): void {
|
||||
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 {
|
||||
throw new Error('Method not implemented.');
|
||||
predicate = predicate.next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const requestedRead = readFromNodeInjector(nodeInjector, node, predicate.read);
|
||||
if (requestedRead !== null) {
|
||||
predicate.values.push(requestedRead);
|
||||
addMatch(predicate, requestedRead);
|
||||
}
|
||||
} else {
|
||||
predicate.values.push(node.view.data[directiveIdx]);
|
||||
addMatch(predicate, node.view.data[directiveIdx]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -207,10 +264,10 @@ function add(predicate: QueryPredicate<any>| null, node: LNode) {
|
|||
if (predicate.read !== null) {
|
||||
const result = readFromNodeInjector(nodeInjector, node, predicate.read !, directiveIdx);
|
||||
if (result !== null) {
|
||||
predicate.values.push(result);
|
||||
addMatch(predicate, result);
|
||||
}
|
||||
} 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>(
|
||||
previous: QueryPredicate<any>| null, queryList: QueryList<T>, predicate: Type<T>| string[],
|
||||
read: QueryReadType<T>| Type<T>| null): QueryPredicate<T> {
|
||||
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 {
|
||||
next: previous,
|
||||
list: queryList,
|
||||
type: isArray ? null : predicate as Type<T>,
|
||||
selector: isArray ? predicate as string[] : null,
|
||||
read: read,
|
||||
values: values
|
||||
values: (queryList as any as QueryList_<T>)._valuesTree
|
||||
};
|
||||
}
|
||||
|
||||
class QueryList_<T>/* implements viewEngine_QueryList<T> */ {
|
||||
dirty: boolean = false;
|
||||
changes: Observable<T>;
|
||||
readonly dirty = true;
|
||||
readonly changes: Observable<T>;
|
||||
private _values: T[]|null = null;
|
||||
/** @internal */
|
||||
_valuesTree: any[] = [];
|
||||
|
||||
get length(): number {
|
||||
ngDevMode && assertNotNull(this._values, 'refreshed');
|
||||
|
@ -258,21 +319,6 @@ class QueryList_<T>/* implements viewEngine_QueryList<T> */ {
|
|||
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[] {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
@ -295,10 +341,16 @@ class QueryList_<T>/* implements viewEngine_QueryList<T> */ {
|
|||
ngDevMode && assertNotNull(this._values, 'refreshed');
|
||||
return this._values !;
|
||||
}
|
||||
toString(): string { throw new Error('Method not implemented.'); }
|
||||
reset(res: (any[]|T)[]): void { throw new Error('Method not implemented.'); }
|
||||
toString(): string {
|
||||
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.'); }
|
||||
setDirty(): void { throw new Error('Method not implemented.'); }
|
||||
setDirty(): void { (this as{dirty: boolean}).dirty = true; }
|
||||
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.
|
||||
export type QueryList<T> = viewEngine_QueryList<T>;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -31,3 +31,28 @@ export function stringify(value: any): string {
|
|||
export function notImplemented(): Error {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
* 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 {C, E, Q, QueryList, e, m, qR} from '../../src/render3/index';
|
||||
import {QueryReadType} from '../../src/render3/interfaces/query';
|
||||
import {C, E, Q, QueryList, V, cR, cr, detectChanges, e, m, qR, v} from '../../src/render3/index';
|
||||
|
||||
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', () => {
|
||||
const Child = createDirective();
|
||||
|
||||
let childInstance, div;
|
||||
/**
|
||||
* <div #foo></div>
|
||||
* class Cmpt {
|
||||
|
@ -522,7 +520,7 @@ describe('query', () => {
|
|||
let tmp: any;
|
||||
if (cm) {
|
||||
m(0, Q(['foo'], false, Child));
|
||||
div = E(1, 'div', null, null, ['foo', '']);
|
||||
E(1, 'div', null, null, ['foo', '']);
|
||||
e();
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* 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', () => {
|
||||
|
||||
|
@ -32,4 +32,20 @@ describe('util', () => {
|
|||
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]);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue