fix(ivy): support static ViewChild queries (#28811)

This commit adds support for the `static: true` flag in
`ViewChild` queries. Prior to this commit, all `ViewChild`
queries were resolved after change detection ran. This is
a problem for backwards compatibility because View Engine
also supported "static" queries which would resolve before
change detection.

Now if users add a `static: true` option, the query will be
resolved in creation mode (before change detection runs).
For example:

```ts
@ViewChild(TemplateRef, {static: true}) template !: TemplateRef;
```

This feature will come in handy for components that need
to create components dynamically.

PR Close #28811
This commit is contained in:
Kara Erickson 2019-02-18 17:33:59 -08:00 committed by Igor Minar
parent ae16378ee7
commit a4638d5a81
26 changed files with 340 additions and 163 deletions

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 12885,
"main": 13019,
"polyfills": 38390
}
}

View File

@ -229,6 +229,9 @@ export function extractQueryMetadata(
const node = unwrapForwardRef(args[0], reflector);
const arg = evaluator.evaluate(node);
/** Whether or not this query should collect only static results (see view/api.ts) */
let isStatic: boolean = false;
// Extract the predicate
let predicate: Expression|string[]|null = null;
if (arg instanceof Reference) {
@ -263,13 +266,28 @@ export function extractQueryMetadata(
}
descendants = descendantsValue;
}
if (options.has('static')) {
const staticValue = evaluator.evaluate(options.get('static') !);
if (typeof staticValue !== 'boolean') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, node, `@${name} options.static must be a boolean`);
}
isStatic = staticValue;
}
} else if (args.length > 2) {
// Too many arguments.
throw new Error(`@${name} has too many arguments`);
}
return {
propertyName, predicate, first, descendants, read,
propertyName,
predicate,
first,
descendants,
read,
static: isStatic,
};
}

View File

@ -1376,8 +1376,8 @@ describe('compiler compliance', () => {
factory: function ViewQueryComponent_Factory(t) { return new (t || ViewQueryComponent)(); },
viewQuery: function ViewQueryComponent_Query(rf, ctx) {
if (rf & 1) {
$r3$.ɵviewQuery(SomeDirective, true);
$r3$.ɵviewQuery(SomeDirective, true);
$r3$.ɵviewQuery(SomeDirective, true, null);
$r3$.ɵviewQuery(SomeDirective, true, null);
}
if (rf & 2) {
var $tmp$;
@ -1434,8 +1434,8 @@ describe('compiler compliance', () => {
viewQuery: function ViewQueryComponent_Query(rf, ctx) {
if (rf & 1) {
$r3$.ɵviewQuery($e0_attrs$, true);
$r3$.ɵviewQuery($e1_attrs$, true);
$r3$.ɵviewQuery($e0_attrs$, true, null);
$r3$.ɵviewQuery($e1_attrs$, true, null);
}
if (rf & 2) {
var $tmp$;
@ -1452,6 +1452,67 @@ describe('compiler compliance', () => {
expectEmit(source, ViewQueryComponentDefinition, 'Invalid ViewQuery declaration');
});
it('should support static view queries', () => {
const files = {
app: {
...directive,
'view_query.component.ts': `
import {Component, NgModule, ViewChild} from '@angular/core';
import {SomeDirective} from './some.directive';
@Component({
selector: 'view-query-component',
template: \`
<div someDir></div>
\`
})
export class ViewQueryComponent {
@ViewChild(SomeDirective, {static: true}) someDir !: SomeDirective;
@ViewChild('foo', {static: false}) foo !: ElementRef;
}
@NgModule({declarations: [SomeDirective, ViewQueryComponent]})
export class MyModule {}
`
}
};
const ViewQueryComponentDefinition = `
const $refs$ = ["foo"];
const $e0_attrs$ = ["someDir",""];
ViewQueryComponent.ngComponentDef = $r3$.ɵdefineComponent({
type: ViewQueryComponent,
selectors: [["view-query-component"]],
factory: function ViewQueryComponent_Factory(t) { return new (t || ViewQueryComponent)(); },
viewQuery: function ViewQueryComponent_Query(rf, ctx) {
if (rf & 1) {
$r3$.ɵstaticViewQuery(SomeDirective, true, null);
$r3$.ɵviewQuery($refs$, true, null);
}
if (rf & 2) {
var $tmp$;
($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadViewQuery())) && (ctx.someDir = $tmp$.first));
($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadViewQuery())) && (ctx.foo = $tmp$.first));
}
},
consts: 1,
vars: 0,
template: function ViewQueryComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵelement(0, "div", $e0_attrs$);
}
},
directives: function () { return [SomeDirective]; },
encapsulation: 2
});`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, ViewQueryComponentDefinition, 'Invalid ViewQuery declaration');
});
it('should support view queries with read tokens specified', () => {
const files = {
app: {
@ -1555,8 +1616,8 @@ describe('compiler compliance', () => {
},
contentQueries: function ContentQueryComponent_ContentQueries(rf, ctx, dirIndex) {
if (rf & 1) {
$r3$.ɵcontentQuery(dirIndex, SomeDirective, true);
$r3$.ɵcontentQuery(dirIndex, SomeDirective, false);
$r3$.ɵcontentQuery(dirIndex, SomeDirective, true, null);
$r3$.ɵcontentQuery(dirIndex, SomeDirective, false, null);
}
if (rf & 2) {
var $tmp$;
@ -1615,8 +1676,8 @@ describe('compiler compliance', () => {
contentQueries: function ContentQueryComponent_ContentQueries(rf, ctx, dirIndex) {
if (rf & 1) {
$r3$.ɵcontentQuery(dirIndex, $e0_attrs$, true);
$r3$.ɵcontentQuery(dirIndex, $e1_attrs$, false);
$r3$.ɵcontentQuery(dirIndex, $e0_attrs$, true, null);
$r3$.ɵcontentQuery(dirIndex, $e1_attrs$, false, null);
}
if (rf & 2) {
var $tmp$;

View File

@ -17,13 +17,13 @@ const trim = (input: string): string => input.replace(/\s+/g, ' ').trim();
const varRegExp = (name: string): RegExp => new RegExp(`var \\w+ = \\[\"${name}\"\\];`);
const viewQueryRegExp = (descend: boolean, ref?: string): RegExp => {
const maybeRef = ref ? `, ${ref}` : ``;
return new RegExp(`i0\\.ɵviewQuery\\(\\w+, ${descend}${maybeRef}\\)`);
const maybeRef = ref ? `${ref}` : `null`;
return new RegExp(`i0\\.ɵviewQuery\\(\\w+, ${descend}, ${maybeRef}\\)`);
};
const contentQueryRegExp = (predicate: string, descend: boolean, ref?: string): RegExp => {
const maybeRef = ref ? `, ${ref}` : ``;
return new RegExp(`i0\\.ɵcontentQuery\\(dirIndex, ${predicate}, ${descend}${maybeRef}\\)`);
const maybeRef = ref ? `${ref}` : `null`;
return new RegExp(`i0\\.ɵcontentQuery\\(dirIndex, ${predicate}, ${descend}, ${maybeRef}\\)`);
};
describe('ngtsc behavioral tests', () => {
@ -1017,7 +1017,7 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents).toMatch(varRegExp('accessor'));
// match `i0.ɵcontentQuery(dirIndex, _c1, true, TemplateRef)`
expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef'));
// match `i0.ɵviewQuery(_c2, true)`
// match `i0.ɵviewQuery(_c2, true, null)`
expect(jsContents).toMatch(viewQueryRegExp(true));
});
@ -1039,9 +1039,9 @@ describe('ngtsc behavioral tests', () => {
env.driveMain();
const jsContents = env.getContents('test.js');
// match `i0.ɵcontentQuery(dirIndex, TemplateRef, true)`
// match `i0.ɵcontentQuery(dirIndex, TemplateRef, true, null)`
expect(jsContents).toMatch(contentQueryRegExp('TemplateRef', true));
// match `i0.ɵcontentQuery(dirIndex, ViewContainerRef, true)`
// match `i0.ɵcontentQuery(dirIndex, ViewContainerRef, true, null)`
expect(jsContents).toMatch(contentQueryRegExp('ViewContainerRef', true));
});

View File

@ -150,6 +150,7 @@ export interface R3QueryMetadataFacade {
predicate: any|string[];
descendants: boolean;
read: any|null;
static: boolean;
}
export interface ParseSourceSpan {

View File

@ -199,6 +199,7 @@ function convertToR3QueryMetadata(facade: R3QueryMetadataFacade): R3QueryMetadat
predicate: Array.isArray(facade.predicate) ? facade.predicate :
new WrappedNodeExpr(facade.predicate),
read: facade.read ? new WrappedNodeExpr(facade.read) : null,
static: facade.static
};
}

View File

@ -186,6 +186,7 @@ export class Identifiers {
static queryRefresh: o.ExternalReference = {name: 'ɵqueryRefresh', moduleName: CORE};
static viewQuery: o.ExternalReference = {name: 'ɵviewQuery', moduleName: CORE};
static staticViewQuery: o.ExternalReference = {name: 'ɵstaticViewQuery', moduleName: CORE};
static loadViewQuery: o.ExternalReference = {name: 'ɵloadViewQuery', moduleName: CORE};
static contentQuery: o.ExternalReference = {name: 'ɵcontentQuery', moduleName: CORE};
static loadContentQuery: o.ExternalReference = {name: 'ɵloadContentQuery', moduleName: CORE};

View File

@ -229,6 +229,21 @@ export interface R3QueryMetadata {
* for a given node is to be returned.
*/
read: o.Expression|null;
/**
* Whether or not this query should collect only static results.
*
* If static is true, the query's results will be set on the component after nodes are created,
* but before change detection runs. This means that any results that relied upon change detection
* to run (e.g. results inside *ngIf or *ngFor views) will not be collected. Query results are
* available in the ngOnInit hook.
*
* If static is false, the query's results will be set on the component after change detection
* runs. This means that the query results can contain nodes inside *ngIf or *ngFor views, but
* the results will not be available in the ngOnInit hook (only in the ngAfterContentInit for
* content hooks and ngAfterViewInit for view hooks).
*/
static: boolean;
}
/**

View File

@ -457,6 +457,7 @@ function queriesFromGlobalMetadata(
first: query.first,
predicate: selectorsFromGlobalMetadata(query.selectors, outputCtx),
descendants: query.descendants, read,
static: !!query.static
};
});
}
@ -490,10 +491,8 @@ function prepareQueryParams(query: R3QueryMetadata, constantPool: ConstantPool):
const parameters = [
getQueryPredicate(query, constantPool),
o.literal(query.descendants),
query.read || o.literal(null),
];
if (query.read) {
parameters.push(query.read);
}
return parameters;
}
@ -590,9 +589,11 @@ function createViewQueriesFunction(
const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME);
meta.viewQueries.forEach((query: R3QueryMetadata) => {
const queryInstruction = query.static ? R3.staticViewQuery : R3.viewQuery;
// creation, e.g. r3.viewQuery(somePredicate, true);
const queryDefinition =
o.importExpr(R3.viewQuery).callFn(prepareQueryParams(query, constantPool));
o.importExpr(queryInstruction).callFn(prepareQueryParams(query, constantPool));
createStatements.push(queryDefinition.toStmt());
// update, e.g. (r3.queryRefresh(tmp = r3.loadViewQuery()) && (ctx.someDir = tmp));

View File

@ -150,6 +150,7 @@ export interface R3QueryMetadataFacade {
predicate: any|string[];
descendants: boolean;
read: any|null;
static: boolean;
}
export interface ParseSourceSpan {

View File

@ -81,6 +81,7 @@ export {
containerRefreshEnd as ɵcontainerRefreshEnd,
queryRefresh as ɵqueryRefresh,
viewQuery as ɵviewQuery,
staticViewQuery as ɵstaticViewQuery,
loadViewQuery as ɵloadViewQuery,
contentQuery as ɵcontentQuery,
loadContentQuery as ɵloadContentQuery,

View File

@ -124,6 +124,7 @@ export {
export {
queryRefresh,
viewQuery,
staticViewQuery,
loadViewQuery,
contentQuery,
loadContentQuery,

View File

@ -37,7 +37,7 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTEXT, DECLARATION_VIEW, Expa
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state';
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode,} from './state';
import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {BoundPlayerFactory} from './styling/player_factory';
import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput, hasStyling, isAnimationProp} from './styling/util';
@ -784,6 +784,7 @@ export function createTView(
expandoStartIndex: initialViewLength,
expandoInstructions: null,
firstTemplatePass: true,
staticViewQueries: false,
initHooks: null,
checkHooks: null,
contentHooks: null,
@ -2922,20 +2923,23 @@ export function checkView<T>(hostView: LView, component: T) {
try {
namespaceHTML();
creationMode && executeViewQueryFn(hostView, hostTView, component);
creationMode && executeViewQueryFn(RenderFlags.Create, hostTView, component);
templateFn(getRenderFlags(hostView), component);
refreshDescendantViews(hostView);
!creationMode && executeViewQueryFn(hostView, hostTView, component);
// Only check view queries again in creation mode if there are static view queries
if (!creationMode || hostTView.staticViewQueries) {
executeViewQueryFn(RenderFlags.Update, hostTView, component);
}
} finally {
leaveView(oldView);
}
}
function executeViewQueryFn<T>(lView: LView, tView: TView, component: T): void {
function executeViewQueryFn<T>(flags: RenderFlags, tView: TView, component: T): void {
const viewQuery = tView.viewQuery;
if (viewQuery) {
setCurrentQueryIndex(tView.viewQueryStartIndex);
viewQuery(getRenderFlags(lView), component);
viewQuery(flags, component);
}
}

View File

@ -376,6 +376,14 @@ export interface TView {
*/
expandoStartIndex: number;
/**
* Whether or not there are any static view queries tracked on this view.
*
* We store this so we know whether or not we should do a view query
* refresh after creation mode to collect static query results.
*/
staticViewQueries: boolean;
/**
* The index where the viewQueries section of `LView` begins. This section contains
* view queries defined for a component/directive.

View File

@ -169,7 +169,8 @@ export function convertToR3QueryMetadata(propertyName: string, ann: Query): R3Qu
predicate: convertToR3QueryPredicate(ann.selector),
descendants: ann.descendants,
first: ann.first,
read: ann.read ? ann.read : null
read: ann.read ? ann.read : null,
static: !!ann.static
};
}
function extractQueriesMetadata(

View File

@ -88,6 +88,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵpipe': r3.pipe,
'ɵqueryRefresh': r3.queryRefresh,
'ɵviewQuery': r3.viewQuery,
'ɵstaticViewQuery': r3.staticViewQuery,
'ɵloadViewQuery': r3.loadViewQuery,
'ɵcontentQuery': r3.contentQuery,
'ɵloadContentQuery': r3.loadContentQuery,

View File

@ -24,7 +24,7 @@ import {unusedValueExportToPlacateAjd as unused2} from './interfaces/injector';
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, unusedValueExportToPlacateAjd as unused3} from './interfaces/node';
import {LQueries, unusedValueExportToPlacateAjd as unused4} from './interfaces/query';
import {CONTENT_QUERIES, HEADER_OFFSET, LView, QUERIES, TVIEW} from './interfaces/view';
import {getCurrentQueryIndex, getIsParent, getLView, setCurrentQueryIndex} from './state';
import {getCurrentQueryIndex, getIsParent, getLView, isCreationMode, setCurrentQueryIndex} from './state';
import {createElementRef, createTemplateRef} from './view_engine_compatibility';
const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4;
@ -338,7 +338,7 @@ function createQuery<T>(
};
}
type QueryList_<T> = QueryList<T>& {_valuesTree: any[]};
type QueryList_<T> = QueryList<T>& {_valuesTree: any[], _static: boolean};
/**
* Creates and returns a QueryList.
@ -350,12 +350,13 @@ type QueryList_<T> = QueryList<T>& {_valuesTree: any[]};
*/
export function query<T>(
// TODO: "read" should be an AbstractType (FW-486)
predicate: Type<any>| string[], descend?: boolean, read?: any): QueryList<T> {
predicate: Type<any>| string[], descend: boolean, read: any): QueryList<T> {
ngDevMode && assertPreviousIsParent(getIsParent());
const lView = getLView();
const queryList = new QueryList<T>();
const queryList = new QueryList<T>() as QueryList_<T>;
const queries = lView[QUERIES] || (lView[QUERIES] = new LQueries_(null, null, null));
(queryList as QueryList_<T>)._valuesTree = [];
queryList._valuesTree = [];
queryList._static = false;
queries.track(queryList, predicate, descend, read);
storeCleanupWithContext(lView, queryList, queryList.destroy);
return queryList;
@ -368,7 +369,10 @@ export function query<T>(
*/
export function queryRefresh(queryList: QueryList<any>): boolean {
const queryListImpl = (queryList as any as QueryList_<any>);
if (queryList.dirty) {
const creationMode = isCreationMode();
// if creation mode and static or update mode and not static
if (queryList.dirty && creationMode === queryListImpl._static) {
queryList.reset(queryListImpl._valuesTree || []);
queryList.notifyOnChanges();
return true;
@ -376,6 +380,24 @@ export function queryRefresh(queryList: QueryList<any>): boolean {
return false;
}
/**
* Creates new QueryList for a static view query.
*
* @param predicate The type for which the query will search
* @param descend Whether or not to descend into children
* @param read What to save in the query
*/
export function staticViewQuery<T>(
// TODO(FW-486): "read" should be an AbstractType
predicate: Type<any>| string[], descend: boolean, read: any): void {
const queryList = viewQuery(predicate, descend, read) as QueryList_<T>;
const tView = getLView()[TVIEW];
queryList._static = true;
if (!tView.staticViewQueries) {
tView.staticViewQueries = true;
}
}
/**
* Creates new QueryList, stores the reference in LView and returns QueryList.
*
@ -385,8 +407,8 @@ export function queryRefresh(queryList: QueryList<any>): boolean {
* @returns QueryList<T>
*/
export function viewQuery<T>(
// TODO: "read" should be an AbstractType (FW-486)
predicate: Type<any>| string[], descend?: boolean, read?: any): QueryList<T> {
// TODO(FW-486): "read" should be an AbstractType
predicate: Type<any>| string[], descend: boolean, read: any): QueryList<T> {
const lView = getLView();
const tView = lView[TVIEW];
if (tView.firstTemplatePass) {
@ -419,9 +441,9 @@ export function loadViewQuery<T>(): T {
* @returns QueryList<T>
*/
export function contentQuery<T>(
directiveIndex: number, predicate: Type<any>| string[], descend?: boolean,
directiveIndex: number, predicate: Type<any>| string[], descend: boolean,
// TODO: "read" should be an AbstractType (FW-486)
read?: any): QueryList<T> {
read: any): QueryList<T> {
const lView = getLView();
const tView = lView[TVIEW];
const contentQuery: QueryList<T> = query<T>(predicate, descend, read);
@ -448,4 +470,4 @@ export function loadContentQuery<T>(): QueryList<T> {
setCurrentQueryIndex(index + 1);
return lView[CONTENT_QUERIES] ![index];
}
}

View File

@ -16,7 +16,8 @@ describe('query logic', () => {
TestBed.configureTestingModule({
declarations: [
AppComp, QueryComp, SimpleCompA, SimpleCompB, StaticViewQueryComp, TextDirective,
SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp
SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp,
QueryCompWithChanges
]
});
});
@ -87,48 +88,78 @@ describe('query logic', () => {
expect(comp.viewChildren.length).toBe(2);
});
fixmeIvy('Must support static view queries in Ivy')
.it('should set static view child queries in creation mode (and just in creation mode)',
() => {
const fixture = TestBed.createComponent(StaticViewQueryComp);
const component = fixture.componentInstance;
it('should set static view child queries in creation mode (and just in creation mode)', () => {
const fixture = TestBed.createComponent(StaticViewQueryComp);
const component = fixture.componentInstance;
// static ViewChild query should be set in creation mode, before CD runs
expect(component.textDir).toBeAnInstanceOf(TextDirective);
expect(component.textDir.text).toEqual('');
expect(component.setEvents).toEqual(['textDir set']);
// static ViewChild query should be set in creation mode, before CD runs
expect(component.textDir).toBeAnInstanceOf(TextDirective);
expect(component.textDir.text).toEqual('');
expect(component.setEvents).toEqual(['textDir set']);
// dynamic ViewChild query should not have been resolved yet
expect(component.foo).not.toBeDefined();
// dynamic ViewChild query should not have been resolved yet
expect(component.foo).not.toBeDefined();
const span = fixture.nativeElement.querySelector('span');
fixture.detectChanges();
expect(component.textDir.text).toEqual('some text');
expect(component.foo.nativeElement).toBe(span);
expect(component.setEvents).toEqual(['textDir set', 'foo set']);
});
const span = fixture.nativeElement.querySelector('span');
fixture.detectChanges();
expect(component.textDir.text).toEqual('some text');
expect(component.foo.nativeElement).toBe(span);
expect(component.setEvents).toEqual(['textDir set', 'foo set']);
});
fixmeIvy('Must support static view queries in Ivy')
.it('should support static view child queries inherited from superclasses', () => {
const fixture = TestBed.createComponent(SubclassStaticViewQueryComp);
const component = fixture.componentInstance;
const divs = fixture.nativeElement.querySelectorAll('div');
const spans = fixture.nativeElement.querySelectorAll('span');
it('should support static view child queries inherited from superclasses', () => {
const fixture = TestBed.createComponent(SubclassStaticViewQueryComp);
const component = fixture.componentInstance;
const divs = fixture.nativeElement.querySelectorAll('div');
const spans = fixture.nativeElement.querySelectorAll('span');
// static ViewChild queries should be set in creation mode, before CD runs
expect(component.textDir).toBeAnInstanceOf(TextDirective);
expect(component.textDir.text).toEqual('');
expect(component.bar.nativeElement).toEqual(divs[1]);
// static ViewChild queries should be set in creation mode, before CD runs
expect(component.textDir).toBeAnInstanceOf(TextDirective);
expect(component.textDir.text).toEqual('');
expect(component.bar.nativeElement).toEqual(divs[1]);
// dynamic ViewChild queries should not have been resolved yet
expect(component.foo).not.toBeDefined();
expect(component.baz).not.toBeDefined();
// dynamic ViewChild queries should not have been resolved yet
expect(component.foo).not.toBeDefined();
expect(component.baz).not.toBeDefined();
fixture.detectChanges();
expect(component.textDir.text).toEqual('some text');
expect(component.foo.nativeElement).toBe(spans[0]);
expect(component.baz.nativeElement).toBe(spans[1]);
});
fixture.detectChanges();
expect(component.textDir.text).toEqual('some text');
expect(component.foo.nativeElement).toBe(spans[0]);
expect(component.baz.nativeElement).toBe(spans[1]);
});
it('should support multiple static view queries (multiple template passes)', () => {
const template = `
<static-view-query-comp></static-view-query-comp>
<static-view-query-comp></static-view-query-comp>
`;
TestBed.overrideComponent(AppComp, {set: new Component({template})});
const fixture = TestBed.createComponent(AppComp);
const firstComponent = fixture.debugElement.children[0].injector.get(StaticViewQueryComp);
const secondComponent = fixture.debugElement.children[1].injector.get(StaticViewQueryComp);
// static ViewChild query should be set in creation mode, before CD runs
expect(firstComponent.textDir).toBeAnInstanceOf(TextDirective);
expect(secondComponent.textDir).toBeAnInstanceOf(TextDirective);
expect(firstComponent.textDir.text).toEqual('');
expect(secondComponent.textDir.text).toEqual('');
expect(firstComponent.setEvents).toEqual(['textDir set']);
expect(secondComponent.setEvents).toEqual(['textDir set']);
// dynamic ViewChild query should not have been resolved yet
expect(firstComponent.foo).not.toBeDefined();
expect(secondComponent.foo).not.toBeDefined();
const spans = fixture.nativeElement.querySelectorAll('span');
fixture.detectChanges();
expect(firstComponent.textDir.text).toEqual('some text');
expect(secondComponent.textDir.text).toEqual('some text');
expect(firstComponent.foo.nativeElement).toBe(spans[0]);
expect(secondComponent.foo.nativeElement).toBe(spans[1]);
expect(firstComponent.setEvents).toEqual(['textDir set', 'foo set']);
expect(secondComponent.setEvents).toEqual(['textDir set', 'foo set']);
});
});
@ -290,6 +321,30 @@ describe('query logic', () => {
expect(component.baz.nativeElement).toBe(spans[1]);
});
describe('observable interface', () => {
it('should allow observing changes to query list', () => {
const fixture = TestBed.createComponent(QueryCompWithChanges);
let changes = 0;
fixture.detectChanges();
fixture.componentInstance.foos.changes.subscribe((value: any) => {
changes += 1;
expect(value).toBe(fixture.componentInstance.foos);
});
// refresh without setting dirty - no emit
fixture.detectChanges();
expect(changes).toBe(0);
// refresh with setting dirty - emit
fixture.componentInstance.showing = true;
fixture.detectChanges();
expect(changes).toBe(1);
});
});
});
function initWithTemplate(compType: Type<any>, template: string) {
@ -406,3 +461,15 @@ class SubclassStaticContentQueryComp extends StaticContentQueryComp {
@ContentChild('baz', {static: false})
baz !: ElementRef;
}
@Component({
selector: 'query-with-changes',
template: `
<div *ngIf="showing" #foo></div>
`
})
export class QueryCompWithChanges {
@ViewChildren('foo') foos !: QueryList<any>;
showing = false;
}

View File

@ -126,7 +126,7 @@ describe('Query API', () => {
]);
});
modifiedInIvy('Static ViewChild and ContentChild queries are resolved in update mode')
modifiedInIvy('Static queries in Ivy require an explicit {static: true} arg')
.it('should set static view and content children already after the constructor call', () => {
const template =
'<needs-static-content-view-child #q><div text="contentFoo"></div></needs-static-content-view-child>';

View File

@ -1026,7 +1026,7 @@ describe('content projection', () => {
function(rf: RenderFlags, ctx: any) {
/** @ViewChild(TemplateRef) template: TemplateRef<any> */
if (rf & RenderFlags.Create) {
viewQuery(TemplateRef as any, true);
viewQuery(TemplateRef as any, true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;

View File

@ -1011,7 +1011,7 @@ describe('host bindings', () => {
},
contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => {
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo']);
contentQuery(dirIndex, ['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;

View File

@ -382,7 +382,7 @@ describe('InheritDefinitionFeature', () => {
selectors: [['super-comp']],
viewQuery: <T>(rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
viewQuery(['super'], false);
viewQuery(['super'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -418,7 +418,7 @@ describe('InheritDefinitionFeature', () => {
selectors: [['sub-comp']],
viewQuery: (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
viewQuery(['sub'], false);
viewQuery(['sub'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -561,7 +561,7 @@ describe('InheritDefinitionFeature', () => {
factory: () => new SuperDirective(),
contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => {
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo'], true);
contentQuery(dirIndex, ['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -581,7 +581,7 @@ describe('InheritDefinitionFeature', () => {
factory: () => dirInstance = new SubDirective(),
contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => {
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['bar'], true);
contentQuery(dirIndex, ['bar'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;

View File

@ -56,7 +56,8 @@ describe('jit directive helper functions', () => {
predicate: ['localRef'],
descendants: false,
first: false,
read: null
read: null,
static: false
});
});
@ -72,7 +73,8 @@ describe('jit directive helper functions', () => {
predicate: ['foo', 'bar', 'baz'],
descendants: true,
first: true,
read: null
read: null,
static: false
});
});
@ -85,7 +87,8 @@ describe('jit directive helper functions', () => {
descendants: true,
first: true,
isViewQuery: true,
read: Directive
read: Directive,
static: false
});
expect(converted.predicate).toEqual(Directive);

View File

@ -15,7 +15,7 @@ import {getNativeByIndex} from '../../src/render3/util';
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, reference, template, text} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {queryRefresh, viewQuery, loadViewQuery, contentQuery, loadContentQuery} from '../../src/render3/query';
import {queryRefresh, viewQuery, loadViewQuery, contentQuery, loadContentQuery, query} from '../../src/render3/query';
import {getLView} from '../../src/render3/state';
import {templateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound';
@ -83,8 +83,8 @@ describe('query', () => {
2, 0, [Child], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(Child, false);
viewQuery(Child, true);
viewQuery(Child, false, null);
viewQuery(Child, true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -261,9 +261,9 @@ describe('query', () => {
},
viewQuery: function(rf: RenderFlags, ctx: App) {
if (rf & RenderFlags.Create) {
viewQuery(MyDirective, false);
viewQuery(Service, false);
viewQuery(Alias, false);
viewQuery(MyDirective, false, null);
viewQuery(Service, false, null);
viewQuery(Alias, false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -347,7 +347,7 @@ describe('query', () => {
3, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], false);
viewQuery(['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -385,8 +385,8 @@ describe('query', () => {
4, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], false);
viewQuery(['bar'], false);
viewQuery(['foo'], false, null);
viewQuery(['bar'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -434,7 +434,7 @@ describe('query', () => {
5, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo', 'bar'], undefined);
viewQuery(['foo', 'bar'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -472,7 +472,7 @@ describe('query', () => {
3, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], false);
viewQuery(['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -545,7 +545,7 @@ describe('query', () => {
2, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -746,7 +746,7 @@ describe('query', () => {
2, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], undefined);
viewQuery(['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -817,7 +817,7 @@ describe('query', () => {
2, 0, [Child], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -863,7 +863,7 @@ describe('query', () => {
2, 0, [Child], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -902,7 +902,7 @@ describe('query', () => {
2, 0, [Child], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -942,7 +942,7 @@ describe('query', () => {
3, 0, [Child1, Child2], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo', 'bar'], true);
viewQuery(['foo', 'bar'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -982,8 +982,8 @@ describe('query', () => {
3, 0, [Child], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['bar'], true);
viewQuery(['foo'], true, null);
viewQuery(['bar'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -1026,7 +1026,7 @@ describe('query', () => {
2, 0, [Child], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], undefined, ElementRef);
viewQuery(['foo'], false, ElementRef);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -1065,7 +1065,7 @@ describe('query', () => {
3, 0, [Child], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo', 'bar'], undefined);
viewQuery(['foo', 'bar'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -1266,7 +1266,7 @@ describe('query', () => {
1, 0, [Child], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(TemplateRef as any, false);
viewQuery(TemplateRef as any, false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -1315,7 +1315,7 @@ describe('query', () => {
6, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(TemplateRef as any, false);
viewQuery(TemplateRef as any, false, null);
viewQuery(TemplateRef as any, false, ElementRef);
}
if (rf & RenderFlags.Update) {
@ -1401,7 +1401,7 @@ describe('query', () => {
2, 1, [NgIf], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -1463,7 +1463,7 @@ describe('query', () => {
viewQuery: function(rf: RenderFlags, ctx: Cmpt) {
let tmp: any;
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
queryRefresh(tmp = loadViewQuery<QueryList<any>>()) &&
@ -1552,7 +1552,7 @@ describe('query', () => {
8, 0, [ViewContainerManipulatorDirective], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -1649,7 +1649,7 @@ describe('query', () => {
viewQuery: (rf: RenderFlags, cmpt: Cmpt) => {
let tmp: any;
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
queryRefresh(tmp = loadViewQuery<QueryList<any>>()) &&
@ -1722,7 +1722,7 @@ describe('query', () => {
viewQuery: (rf: RenderFlags, myApp: MyApp) => {
let tmp: any;
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
queryRefresh(tmp = loadViewQuery<QueryList<any>>()) &&
@ -1787,7 +1787,7 @@ describe('query', () => {
1, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -1853,7 +1853,7 @@ describe('query', () => {
5, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -1930,7 +1930,7 @@ describe('query', () => {
1, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2011,7 +2011,7 @@ describe('query', () => {
1, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2081,8 +2081,8 @@ describe('query', () => {
3, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], false);
viewQuery(['foo'], true, null);
viewQuery(['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2119,36 +2119,6 @@ describe('query', () => {
});
describe('observable interface', () => {
it('should allow observing changes to query list', () => {
const queryList = new QueryList();
let changes = 0;
queryList.changes.subscribe({
next: (arg) => {
changes += 1;
expect(arg).toBe(queryList);
}
});
// initial refresh, the query should be dirty
queryRefresh(queryList);
expect(changes).toBe(1);
// refresh without setting dirty - no emit
queryRefresh(queryList);
expect(changes).toBe(1);
// refresh with setting dirty - emit
queryList.setDirty();
queryRefresh(queryList);
expect(changes).toBe(2);
});
});
describe('queryList', () => {
it('should be destroyed when the containing view is destroyed', () => {
let queryInstance: QueryList<any>;
@ -2163,7 +2133,7 @@ describe('query', () => {
2, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], false);
viewQuery(['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2244,7 +2214,7 @@ describe('query', () => {
3, 0, [SomeDir], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2284,7 +2254,7 @@ describe('query', () => {
factory: () => withContentInstance = new WithContentDirective(),
contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => {
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo'], true);
contentQuery(dirIndex, ['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2307,7 +2277,7 @@ describe('query', () => {
vars: 0,
contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => {
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo'], false);
contentQuery(dirIndex, ['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2468,7 +2438,7 @@ describe('query', () => {
5, 0, [WithContentDirective], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo', 'bar'], true);
viewQuery(['foo', 'bar'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2509,7 +2479,7 @@ describe('query', () => {
5, 0, [WithContentDirective], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['bar'], true);
viewQuery(['bar'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2535,7 +2505,7 @@ describe('query', () => {
// @ContentChildren('foo, bar, baz', {descendants: true})
// fooBars: QueryList<ElementRef>;
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo', 'bar', 'baz'], true);
contentQuery(dirIndex, ['foo', 'bar', 'baz'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2599,7 +2569,7 @@ describe('query', () => {
// @ContentChildren('foo', {descendants: true})
// fooBars: QueryList<ElementRef>;
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo'], false);
contentQuery(dirIndex, ['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2655,7 +2625,7 @@ describe('query', () => {
// @ContentChildren('foo', {descendants: true})
// fooBars: QueryList<ElementRef>;
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo'], false);
contentQuery(dirIndex, ['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2715,7 +2685,7 @@ describe('query', () => {
// @ContentChildren('foo', {descendants: false})
// foos: QueryList<ElementRef>;
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo'], false);
contentQuery(dirIndex, ['foo'], false, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2736,7 +2706,7 @@ describe('query', () => {
// @ContentChildren('foo', {descendants: true})
// foos: QueryList<ElementRef>;
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, ['foo'], true);
contentQuery(dirIndex, ['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2812,7 +2782,7 @@ describe('query', () => {
// @ContentChildren(TextDirective, {descendants: true})
// texts: QueryList<TextDirective>;
if (rf & RenderFlags.Create) {
contentQuery(dirIndex, TextDirective, true);
contentQuery(dirIndex, TextDirective, true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;
@ -2892,7 +2862,7 @@ describe('query', () => {
viewQuery: function(rf: RenderFlags, ctx: ViewQueryComponent) {
let tmp: any;
if (rf & RenderFlags.Create) {
viewQuery(TextDirective, true);
viewQuery(TextDirective, true, null);
}
if (rf & RenderFlags.Update) {
queryRefresh(tmp = loadViewQuery<QueryList<TextDirective>>()) &&

View File

@ -286,7 +286,7 @@ class SuperComp {
},
viewQuery: function(rf: RenderFlags, ctx: SuperComp) {
if (rf & RenderFlags.Create) {
viewQuery(['child'], true);
viewQuery(['child'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;

View File

@ -2070,7 +2070,7 @@ describe('ViewContainerRef', () => {
},
viewQuery: function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
viewQuery(['foo'], true);
viewQuery(['foo'], true, null);
}
if (rf & RenderFlags.Update) {
let tmp: any;