/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {ElementRef, QueryList, TemplateRef, ViewContainerRef} from '@angular/core'; import {getDebugContext} from '@angular/core/src/errors'; import {NodeDef, NodeFlags, QueryBindingType, QueryValueType, Services, anchorDef, asElementData, asProviderData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, queryDef} from '@angular/core/src/view/index'; import {compViewDef, compViewDefFactory, createAndGetRootNodes, createEmbeddedView} from './helper'; { describe(`Query Views`, () => { const someQueryId = 1; class AService {} class QueryService { // TODO(issue/24571): remove '!'. a !: QueryList; } function contentQueryProviders(checkIndex: number) { return [ directiveDef(checkIndex, NodeFlags.None, null, 1, QueryService, []), queryDef( NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId, {'a': QueryBindingType.All}) ]; } const cQPLength = contentQueryProviders(0).length; // nodes first checkIndex should be 1 (to account for the `queryDef` function compViewQueryProviders(checkIndex: number, extraChildCount: number, nodes: NodeDef[]) { return [ elementDef( checkIndex, NodeFlags.None, null, null, 1 + extraChildCount, 'div', null, null, null, null, () => compViewDef([ queryDef( NodeFlags.TypeViewQuery | NodeFlags.DynamicQuery, someQueryId, {'a': QueryBindingType.All}), ...nodes ])), directiveDef( checkIndex + 1, NodeFlags.Component, null !, 0, QueryService, [], null !, null !, ), ]; } const cVQLength = compViewQueryProviders(0, 0, []).length; function aServiceProvider(checkIndex: number) { return directiveDef( checkIndex, NodeFlags.None, [[someQueryId, QueryValueType.Provider]], 0, AService, []); } describe('content queries', () => { it('should query providers on the same element and child elements', () => { const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, null, null, 5, 'div'), ...contentQueryProviders(1), aServiceProvider(1 + cQPLength), elementDef(2 + cQPLength, NodeFlags.None, null, null, 1, 'div'), aServiceProvider(3 + cQPLength), ])); const qs: QueryService = asProviderData(view, 1).instance; expect(qs.a).toBeUndefined(); Services.checkAndUpdateView(view); const as = qs.a.toArray(); expect(as.length).toBe(2); expect(as[0]).toBe(asProviderData(view, 3).instance); expect(as[1]).toBe(asProviderData(view, 5).instance); }); it('should not query providers on sibling or parent elements', () => { const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, null, null, 6, 'div'), aServiceProvider(1), elementDef(2, NodeFlags.None, null, null, 2, 'div'), ...contentQueryProviders(3), elementDef(3 + cQPLength, NodeFlags.None, null, null, 1, 'div'), aServiceProvider(4 + cQPLength), ])); Services.checkAndUpdateView(view); const qs: QueryService = asProviderData(view, 3).instance; expect(qs.a.length).toBe(0); }); }); describe('view queries', () => { it('should query providers in the view', () => { const {view} = createAndGetRootNodes(compViewDef([ ...compViewQueryProviders( 0, 0, [ elementDef(1, NodeFlags.None, null, null, 1, 'span'), aServiceProvider(2), ]), ])); Services.checkAndUpdateView(view); const comp: QueryService = asProviderData(view, 1).instance; const compView = asElementData(view, 0).componentView; expect(comp.a.length).toBe(1); expect(comp.a.first).toBe(asProviderData(compView, 2).instance); }); it('should not query providers on the host element', () => { const {view} = createAndGetRootNodes(compViewDef([ ...compViewQueryProviders(0, 1, [elementDef(1, NodeFlags.None, null, null, 0, 'span')]), aServiceProvider(cVQLength), ])); Services.checkAndUpdateView(view); const comp: QueryService = asProviderData(view, 1).instance; expect(comp.a.length).toBe(0); }); }); describe('embedded views', () => { it('should query providers in embedded views', () => { const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, null, null, 5, 'div'), ...contentQueryProviders(1), anchorDef(NodeFlags.EmbeddedViews, null, null, 2, null, compViewDefFactory([ elementDef(0, NodeFlags.None, null, null, 1, 'div'), aServiceProvider(1), ])), ...contentQueryProviders(2 + cQPLength), ])); const childView = createEmbeddedView(view, view.def.nodes[3]); attachEmbeddedView(view, asElementData(view, 3), 0, childView); Services.checkAndUpdateView(view); // queries on parent elements of anchors const qs1: QueryService = asProviderData(view, 1).instance; expect(qs1.a.length).toBe(1); expect(qs1.a.first instanceof AService).toBe(true); // queries on the anchor const qs2: QueryService = asProviderData(view, 4).instance; expect(qs2.a.length).toBe(1); expect(qs2.a.first instanceof AService).toBe(true); }); it('should query providers in embedded views only at the template declaration', () => { const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, null, null, 3, 'div'), ...contentQueryProviders(1), anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([ elementDef(0, NodeFlags.None, null, null, 1, 'div'), aServiceProvider(1), ])), elementDef(2 + cQPLength, NodeFlags.None, null, null, 3, 'div'), ...contentQueryProviders(3 + cQPLength), anchorDef(NodeFlags.EmbeddedViews, null, null, 0), ])); const childView = createEmbeddedView(view, view.def.nodes[3]); // attach at a different place than the one where the template was defined attachEmbeddedView(view, asElementData(view, 7), 0, childView); Services.checkAndUpdateView(view); // query on the declaration place const qs1: QueryService = asProviderData(view, 1).instance; expect(qs1.a.length).toBe(1); expect(qs1.a.first instanceof AService).toBe(true); // query on the attach place const qs2: QueryService = asProviderData(view, 5).instance; expect(qs2.a.length).toBe(0); }); it('should update content queries if embedded views are added or removed', () => { const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, null, null, 3, 'div'), ...contentQueryProviders(1), anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([ elementDef(0, NodeFlags.None, null, null, 1, 'div'), aServiceProvider(1), ])), ])); Services.checkAndUpdateView(view); const qs: QueryService = asProviderData(view, 1).instance; expect(qs.a.length).toBe(0); const childView = createEmbeddedView(view, view.def.nodes[3]); attachEmbeddedView(view, asElementData(view, 3), 0, childView); Services.checkAndUpdateView(view); expect(qs.a.length).toBe(1); detachEmbeddedView(asElementData(view, 3), 0); Services.checkAndUpdateView(view); expect(qs.a.length).toBe(0); }); it('should update view queries if embedded views are added or removed', () => { const {view} = createAndGetRootNodes(compViewDef([ ...compViewQueryProviders( 0, 0, [ anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, compViewDefFactory([ elementDef(0, NodeFlags.None, null, null, 1, 'div'), aServiceProvider(1), ])), ]), ])); Services.checkAndUpdateView(view); const comp: QueryService = asProviderData(view, 1).instance; expect(comp.a.length).toBe(0); const compView = asElementData(view, 0).componentView; const childView = createEmbeddedView(compView, compView.def.nodes[1]); attachEmbeddedView(view, asElementData(compView, 1), 0, childView); Services.checkAndUpdateView(view); expect(comp.a.length).toBe(1); detachEmbeddedView(asElementData(compView, 1), 0); Services.checkAndUpdateView(view); expect(comp.a.length).toBe(0); }); }); describe('QueryBindingType', () => { it('should query all matches', () => { class QueryService { // TODO(issue/24571): remove '!'. a !: QueryList; } const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, null, null, 4, 'div'), directiveDef(1, NodeFlags.None, null, 1, QueryService, []), queryDef( NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId, {'a': QueryBindingType.All}), aServiceProvider(3), aServiceProvider(4), ])); Services.checkAndUpdateView(view); const qs: QueryService = asProviderData(view, 1).instance; expect(qs.a instanceof QueryList).toBeTruthy(); expect(qs.a.toArray()).toEqual([ asProviderData(view, 3).instance, asProviderData(view, 4).instance, ]); }); it('should query the first match', () => { class QueryService { // TODO(issue/24571): remove '!'. a !: AService; } const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, null, null, 4, 'div'), directiveDef(1, NodeFlags.None, null, 1, QueryService, []), queryDef( NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId, {'a': QueryBindingType.First}), aServiceProvider(3), aServiceProvider(4), ])); Services.checkAndUpdateView(view); const qs: QueryService = asProviderData(view, 1).instance; expect(qs.a).toBe(asProviderData(view, 3).instance); }); }); describe('query builtins', () => { it('should query ElementRef', () => { class QueryService { // TODO(issue/24571): remove '!'. a !: ElementRef; } const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, [[someQueryId, QueryValueType.ElementRef]], null, 2, 'div'), directiveDef(1, NodeFlags.None, null, 1, QueryService, []), queryDef( NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId, {'a': QueryBindingType.First}), ])); Services.checkAndUpdateView(view); const qs: QueryService = asProviderData(view, 1).instance; expect(qs.a.nativeElement).toBe(asElementData(view, 0).renderElement); }); it('should query TemplateRef', () => { class QueryService { // TODO(issue/24571): remove '!'. a !: TemplateRef; } const {view} = createAndGetRootNodes(compViewDef([ anchorDef( NodeFlags.None, [[someQueryId, QueryValueType.TemplateRef]], null, 2, null, compViewDefFactory([anchorDef(NodeFlags.None, null, null, 0)])), directiveDef(1, NodeFlags.None, null, 1, QueryService, []), queryDef( NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId, {'a': QueryBindingType.First}), ])); Services.checkAndUpdateView(view); const qs: QueryService = asProviderData(view, 1).instance; expect(qs.a.createEmbeddedView).toBeTruthy(); }); it('should query ViewContainerRef', () => { class QueryService { // TODO(issue/24571): remove '!'. a !: ViewContainerRef; } const {view} = createAndGetRootNodes(compViewDef([ anchorDef( NodeFlags.EmbeddedViews, [[someQueryId, QueryValueType.ViewContainerRef]], null, 2), directiveDef(1, NodeFlags.None, null, 1, QueryService, []), queryDef( NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId, {'a': QueryBindingType.First}), ])); Services.checkAndUpdateView(view); const qs: QueryService = asProviderData(view, 1).instance; expect(qs.a.createEmbeddedView).toBeTruthy(); }); }); describe('general binding behavior', () => { it('should report debug info on binding errors', () => { class QueryService { set a(value: any) { throw new Error('Test'); } } const {view} = createAndGetRootNodes(compViewDef([ elementDef(0, NodeFlags.None, null, null, 3, 'div'), directiveDef(1, NodeFlags.None, null, 1, QueryService, []), queryDef( NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId, {'a': QueryBindingType.All}), aServiceProvider(3), ])); let err: any; try { Services.checkAndUpdateView(view); } catch (e) { err = e; } expect(err).toBeTruthy(); expect(err.message).toBe('Test'); const debugCtx = getDebugContext(err); expect(debugCtx.view).toBe(view); expect(debugCtx.nodeIndex).toBe(2); }); }); }); }