402 lines
14 KiB
TypeScript
402 lines
14 KiB
TypeScript
/**
|
|
* @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, Injector, QueryList, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, TemplateRef, ViewContainerRef, ViewEncapsulation, getDebugNode} from '@angular/core';
|
|
import {getDebugContext} from '@angular/core/src/errors';
|
|
import {BindingType, DebugContext, NodeDef, NodeFlags, QueryBindingType, QueryValueType, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, queryDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
|
|
import {inject} from '@angular/core/testing';
|
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
|
|
|
import {createRootView} from './helper';
|
|
|
|
export function main() {
|
|
describe(`Query Views`, () => {
|
|
function compViewDef(
|
|
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
|
|
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
|
|
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
|
|
}
|
|
|
|
function embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinitionFactory {
|
|
return () => viewDef(ViewFlags.None, nodes, update);
|
|
}
|
|
|
|
function createAndGetRootNodes(
|
|
viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} {
|
|
const view = createRootView(viewDef, context);
|
|
const rootNodes = rootRenderNodes(view);
|
|
return {rootNodes, view};
|
|
}
|
|
|
|
const someQueryId = 1;
|
|
|
|
class AService {}
|
|
|
|
class QueryService {
|
|
a: QueryList<AService>;
|
|
}
|
|
|
|
function contentQueryProviders() {
|
|
return [
|
|
directiveDef(NodeFlags.None, null, 1, QueryService, []),
|
|
queryDef(
|
|
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
|
{'a': QueryBindingType.All})
|
|
];
|
|
}
|
|
|
|
function compViewQueryProviders(extraChildCount: number, nodes: NodeDef[]) {
|
|
return [
|
|
elementDef(
|
|
NodeFlags.None, null, null, 1 + extraChildCount, 'div', null, null, null, null,
|
|
() => compViewDef([
|
|
queryDef(
|
|
NodeFlags.TypeViewQuery | NodeFlags.DynamicQuery, someQueryId,
|
|
{'a': QueryBindingType.All}),
|
|
...nodes
|
|
])),
|
|
directiveDef(NodeFlags.Component, null, 0, QueryService, [], null, null, ),
|
|
];
|
|
}
|
|
|
|
function aServiceProvider() {
|
|
return directiveDef(
|
|
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(NodeFlags.None, null, null, 5, 'div'),
|
|
...contentQueryProviders(),
|
|
aServiceProvider(),
|
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
|
aServiceProvider(),
|
|
]));
|
|
|
|
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(NodeFlags.None, null, null, 6, 'div'),
|
|
aServiceProvider(),
|
|
elementDef(NodeFlags.None, null, null, 2, 'div'),
|
|
...contentQueryProviders(),
|
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
|
aServiceProvider(),
|
|
]));
|
|
|
|
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,
|
|
[
|
|
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
|
aServiceProvider(),
|
|
]),
|
|
]));
|
|
|
|
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(
|
|
1,
|
|
[
|
|
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
|
]),
|
|
aServiceProvider(),
|
|
]));
|
|
|
|
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(NodeFlags.None, null, null, 5, 'div'),
|
|
...contentQueryProviders(),
|
|
anchorDef(NodeFlags.EmbeddedViews, null, null, 2, null, embeddedViewDef([
|
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
|
aServiceProvider(),
|
|
])),
|
|
...contentQueryProviders(),
|
|
]));
|
|
|
|
const childView = Services.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(NodeFlags.None, null, null, 3, 'div'),
|
|
...contentQueryProviders(),
|
|
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
|
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
|
aServiceProvider(),
|
|
])),
|
|
elementDef(NodeFlags.None, null, null, 3, 'div'),
|
|
...contentQueryProviders(),
|
|
anchorDef(NodeFlags.EmbeddedViews, null, null, 0),
|
|
]));
|
|
|
|
const childView = Services.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(NodeFlags.None, null, null, 3, 'div'),
|
|
...contentQueryProviders(),
|
|
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
|
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
|
aServiceProvider(),
|
|
])),
|
|
]));
|
|
|
|
Services.checkAndUpdateView(view);
|
|
|
|
const qs: QueryService = asProviderData(view, 1).instance;
|
|
expect(qs.a.length).toBe(0);
|
|
|
|
const childView = Services.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,
|
|
[
|
|
anchorDef(NodeFlags.EmbeddedViews, null, null, 0, null, embeddedViewDef([
|
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
|
aServiceProvider(),
|
|
])),
|
|
]),
|
|
]));
|
|
|
|
Services.checkAndUpdateView(view);
|
|
|
|
const comp: QueryService = asProviderData(view, 1).instance;
|
|
expect(comp.a.length).toBe(0);
|
|
|
|
const compView = asElementData(view, 0).componentView;
|
|
const childView = Services.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 {
|
|
a: QueryList<AService>;
|
|
}
|
|
|
|
const {view} = createAndGetRootNodes(compViewDef([
|
|
elementDef(NodeFlags.None, null, null, 4, 'div'),
|
|
directiveDef(NodeFlags.None, null, 1, QueryService, []),
|
|
queryDef(
|
|
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
|
{'a': QueryBindingType.All}),
|
|
aServiceProvider(),
|
|
aServiceProvider(),
|
|
]));
|
|
|
|
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 {
|
|
a: AService;
|
|
}
|
|
|
|
const {view} = createAndGetRootNodes(compViewDef([
|
|
elementDef(NodeFlags.None, null, null, 4, 'div'),
|
|
directiveDef(NodeFlags.None, null, 1, QueryService, []),
|
|
queryDef(
|
|
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
|
{'a': QueryBindingType.First}),
|
|
aServiceProvider(),
|
|
aServiceProvider(),
|
|
]));
|
|
|
|
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 {
|
|
a: ElementRef;
|
|
}
|
|
|
|
const {view} = createAndGetRootNodes(compViewDef([
|
|
elementDef(NodeFlags.None, [[someQueryId, QueryValueType.ElementRef]], null, 2, 'div'),
|
|
directiveDef(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 {
|
|
a: TemplateRef<any>;
|
|
}
|
|
|
|
const {view} = createAndGetRootNodes(compViewDef([
|
|
anchorDef(
|
|
NodeFlags.None, [[someQueryId, QueryValueType.TemplateRef]], null, 2, null,
|
|
embeddedViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
|
|
directiveDef(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 {
|
|
a: ViewContainerRef;
|
|
}
|
|
|
|
const {view} = createAndGetRootNodes(compViewDef([
|
|
anchorDef(
|
|
NodeFlags.EmbeddedViews, [[someQueryId, QueryValueType.ViewContainerRef]], null, 2),
|
|
directiveDef(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(NodeFlags.None, null, null, 3, 'div'),
|
|
directiveDef(NodeFlags.None, null, 1, QueryService, []),
|
|
queryDef(
|
|
NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
|
|
{'a': QueryBindingType.All}),
|
|
aServiceProvider(),
|
|
]));
|
|
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
}
|