diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index d4aee504a4..92b1b769f7 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -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, { + const 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++; } @@ -1830,4 +1839,4 @@ function assertDataInRange(index: number, arr?: any[]) { function assertDataNext(index: number) { assertEqual(data.length, index, 'data.length not in sequence'); -} \ No newline at end of file +} diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts index fb6d2eecc2..1a5edd8398 100644 --- a/packages/core/src/render3/interfaces/container.ts +++ b/packages/core/src/render3/interfaces/container.ts @@ -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|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; } /** diff --git a/packages/core/src/render3/interfaces/query.ts b/packages/core/src/render3/interfaces/query.ts index 4cc308ea67..b07d8a789e 100644 --- a/packages/core/src/render3/interfaces/query.ts +++ b/packages/core/src/render3/interfaces/query.ts @@ -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. diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index e0bd995f83..3dcc56def9 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -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 */ diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 73dc4daa5e..d3a55d411f 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -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; } diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index f79844030c..e0ddd05508 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -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( - predicate: Type| string[], descend?: boolean, - read?: QueryReadType| Type): QueryList { - ngDevMode && assertPreviousIsParent(); - const queryList = new QueryList(); - const query = getCurrentQuery(LQuery_); - query.track(queryList, predicate, descend, read); - return queryList; -} - -export function queryRefresh(query: QueryList): 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|null = null; + let predicate = this.deep; + + while (predicate) { + const containerValues: any[] = []; // prepare room for views + predicate.values.push(containerValues); + const clonedPredicate: QueryPredicate = { + 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|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 = { + 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(); + } + + predicate = predicate.next; + } } - removeView(container: LContainerNode, view: LViewNode, index: number): void { - 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) => QueryPredicate): LQuery|null { + let result: QueryPredicate|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| 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| 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| null, node: LNode) { } } +function addMatch(predicate: QueryPredicate, matchingValue: any): void { + predicate.values.push(matchingValue); + predicate.list.setDirty(); +} + function createPredicate( previous: QueryPredicate| null, queryList: QueryList, predicate: Type| string[], read: QueryReadType| Type| null): QueryPredicate { const isArray = Array.isArray(predicate); - const values = []; - if ((queryList as any as QueryList_)._valuesTree === null) { - (queryList as any as QueryList_)._valuesTree = values; - } return { next: previous, list: queryList, type: isArray ? null : predicate as Type, selector: isArray ? predicate as string[] : null, read: read, - values: values + values: (queryList as any as QueryList_)._valuesTree }; } class QueryList_/* implements viewEngine_QueryList */ { - dirty: boolean = false; - changes: Observable; + readonly dirty = true; + readonly changes: Observable; + private _values: T[]|null = null; + /** @internal */ + _valuesTree: any[] = []; get length(): number { ngDevMode && assertNotNull(this._values, 'refreshed'); @@ -258,21 +319,6 @@ class QueryList_/* implements viewEngine_QueryList */ { 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(fn: (item: T, index: number, array: T[]) => U): U[] { throw new Error('Method not implemented.'); } @@ -295,10 +341,16 @@ class QueryList_/* implements viewEngine_QueryList */ { 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_/* implements viewEngine_QueryList */ { // it can't be implemented only extended. export type QueryList = viewEngine_QueryList; export const QueryList: typeof viewEngine_QueryList = QueryList_ as any; + +export function query( + predicate: Type| string[], descend?: boolean, + read?: QueryReadType| Type): QueryList { + ngDevMode && assertPreviousIsParent(); + const queryList = new QueryList(); + 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): boolean { + const queryImpl = (query as any as QueryList_); + if (query.dirty) { + query.reset(queryImpl._valuesTree); + return true; + } + return false; +} diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 1ddcd6fa0e..3688f68c89 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -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; +} diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index 3e05dfbde0..16beab207e 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -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; /** *
* 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>(0)) && (ctx.query = tmp as QueryList); @@ -534,4 +532,299 @@ describe('query', () => { }); }); + + describe('view boundaries', () => { + + it('should report results in embedded views', () => { + let firstEl; + /** + * + *
+ *
+ * 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>(0)) && (ctx.query = tmp as QueryList); + }); + + 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; + /** + * + * + *
+ *
+ * + * 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>(0)) && (ctx.query = tmp as QueryList); + }); + + 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; + /** + * + *
+ *
+ * + * + * + * 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>(0)) && (ctx.query = tmp as QueryList); + }); + + 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; + /** + * + *
+ * + * + * + *
+ * 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>(0)) && (ctx.query = tmp as QueryList); + }); + + 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', () => { + /** + * + *
+ *
+ * + * 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>(0)) && (ctx.deep = tmp as QueryList); + qR(tmp = m>(1)) && (ctx.shallow = tmp as QueryList); + }); + + 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); + }); + + }); }); diff --git a/packages/core/test/render3/util_spec.ts b/packages/core/test/render3/util_spec.ts index 097132de07..d591ff1267 100644 --- a/packages/core/test/render3/util_spec.ts +++ b/packages/core/test/render3/util_spec.ts @@ -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(); }); }); -}); \ No newline at end of file + + 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]); + }); + }); +});