Skip to content

Commit a4638d5

Browse files
karaIgorMinar
authored andcommitted
fix(ivy): support static ViewChild queries (angular#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 angular#28811
1 parent ae16378 commit a4638d5

26 files changed

+347
-170
lines changed

integration/_payload-limits.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"master": {
1313
"uncompressed": {
1414
"runtime": 1440,
15-
"main": 12885,
15+
"main": 13019,
1616
"polyfills": 38390
1717
}
1818
}

packages/compiler-cli/src/ngtsc/annotations/src/directive.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ export function extractQueryMetadata(
229229
const node = unwrapForwardRef(args[0], reflector);
230230
const arg = evaluator.evaluate(node);
231231

232+
/** Whether or not this query should collect only static results (see view/api.ts) */
233+
let isStatic: boolean = false;
234+
232235
// Extract the predicate
233236
let predicate: Expression|string[]|null = null;
234237
if (arg instanceof Reference) {
@@ -263,13 +266,28 @@ export function extractQueryMetadata(
263266
}
264267
descendants = descendantsValue;
265268
}
269+
270+
if (options.has('static')) {
271+
const staticValue = evaluator.evaluate(options.get('static') !);
272+
if (typeof staticValue !== 'boolean') {
273+
throw new FatalDiagnosticError(
274+
ErrorCode.VALUE_HAS_WRONG_TYPE, node, `@${name} options.static must be a boolean`);
275+
}
276+
isStatic = staticValue;
277+
}
278+
266279
} else if (args.length > 2) {
267280
// Too many arguments.
268281
throw new Error(`@${name} has too many arguments`);
269282
}
270283

271284
return {
272-
propertyName, predicate, first, descendants, read,
285+
propertyName,
286+
predicate,
287+
first,
288+
descendants,
289+
read,
290+
static: isStatic,
273291
};
274292
}
275293

packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,8 +1376,8 @@ describe('compiler compliance', () => {
13761376
factory: function ViewQueryComponent_Factory(t) { return new (t || ViewQueryComponent)(); },
13771377
viewQuery: function ViewQueryComponent_Query(rf, ctx) {
13781378
if (rf & 1) {
1379-
$r3$.ɵviewQuery(SomeDirective, true);
1380-
$r3$.ɵviewQuery(SomeDirective, true);
1379+
$r3$.ɵviewQuery(SomeDirective, true, null);
1380+
$r3$.ɵviewQuery(SomeDirective, true, null);
13811381
}
13821382
if (rf & 2) {
13831383
var $tmp$;
@@ -1434,8 +1434,8 @@ describe('compiler compliance', () => {
14341434
14351435
viewQuery: function ViewQueryComponent_Query(rf, ctx) {
14361436
if (rf & 1) {
1437-
$r3$.ɵviewQuery($e0_attrs$, true);
1438-
$r3$.ɵviewQuery($e1_attrs$, true);
1437+
$r3$.ɵviewQuery($e0_attrs$, true, null);
1438+
$r3$.ɵviewQuery($e1_attrs$, true, null);
14391439
}
14401440
if (rf & 2) {
14411441
var $tmp$;
@@ -1452,6 +1452,67 @@ describe('compiler compliance', () => {
14521452
expectEmit(source, ViewQueryComponentDefinition, 'Invalid ViewQuery declaration');
14531453
});
14541454

1455+
it('should support static view queries', () => {
1456+
const files = {
1457+
app: {
1458+
...directive,
1459+
'view_query.component.ts': `
1460+
import {Component, NgModule, ViewChild} from '@angular/core';
1461+
import {SomeDirective} from './some.directive';
1462+
1463+
@Component({
1464+
selector: 'view-query-component',
1465+
template: \`
1466+
<div someDir></div>
1467+
\`
1468+
})
1469+
export class ViewQueryComponent {
1470+
@ViewChild(SomeDirective, {static: true}) someDir !: SomeDirective;
1471+
@ViewChild('foo', {static: false}) foo !: ElementRef;
1472+
}
1473+
1474+
@NgModule({declarations: [SomeDirective, ViewQueryComponent]})
1475+
export class MyModule {}
1476+
`
1477+
}
1478+
};
1479+
1480+
const ViewQueryComponentDefinition = `
1481+
const $refs$ = ["foo"];
1482+
const $e0_attrs$ = ["someDir",""];
1483+
1484+
ViewQueryComponent.ngComponentDef = $r3$.ɵdefineComponent({
1485+
type: ViewQueryComponent,
1486+
selectors: [["view-query-component"]],
1487+
factory: function ViewQueryComponent_Factory(t) { return new (t || ViewQueryComponent)(); },
1488+
viewQuery: function ViewQueryComponent_Query(rf, ctx) {
1489+
if (rf & 1) {
1490+
$r3$.ɵstaticViewQuery(SomeDirective, true, null);
1491+
$r3$.ɵviewQuery($refs$, true, null);
1492+
}
1493+
if (rf & 2) {
1494+
var $tmp$;
1495+
($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadViewQuery())) && (ctx.someDir = $tmp$.first));
1496+
($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadViewQuery())) && (ctx.foo = $tmp$.first));
1497+
}
1498+
},
1499+
consts: 1,
1500+
vars: 0,
1501+
template: function ViewQueryComponent_Template(rf, ctx) {
1502+
if (rf & 1) {
1503+
$r3$.ɵelement(0, "div", $e0_attrs$);
1504+
}
1505+
},
1506+
directives: function () { return [SomeDirective]; },
1507+
encapsulation: 2
1508+
});`;
1509+
1510+
const result = compile(files, angularFiles);
1511+
const source = result.source;
1512+
1513+
expectEmit(source, ViewQueryComponentDefinition, 'Invalid ViewQuery declaration');
1514+
});
1515+
14551516
it('should support view queries with read tokens specified', () => {
14561517
const files = {
14571518
app: {
@@ -1555,8 +1616,8 @@ describe('compiler compliance', () => {
15551616
},
15561617
contentQueries: function ContentQueryComponent_ContentQueries(rf, ctx, dirIndex) {
15571618
if (rf & 1) {
1558-
$r3$.ɵcontentQuery(dirIndex, SomeDirective, true);
1559-
$r3$.ɵcontentQuery(dirIndex, SomeDirective, false);
1619+
$r3$.ɵcontentQuery(dirIndex, SomeDirective, true, null);
1620+
$r3$.ɵcontentQuery(dirIndex, SomeDirective, false, null);
15601621
}
15611622
if (rf & 2) {
15621623
var $tmp$;
@@ -1615,8 +1676,8 @@ describe('compiler compliance', () => {
16151676
16161677
contentQueries: function ContentQueryComponent_ContentQueries(rf, ctx, dirIndex) {
16171678
if (rf & 1) {
1618-
$r3$.ɵcontentQuery(dirIndex, $e0_attrs$, true);
1619-
$r3$.ɵcontentQuery(dirIndex, $e1_attrs$, false);
1679+
$r3$.ɵcontentQuery(dirIndex, $e0_attrs$, true, null);
1680+
$r3$.ɵcontentQuery(dirIndex, $e1_attrs$, false, null);
16201681
}
16211682
if (rf & 2) {
16221683
var $tmp$;

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ const trim = (input: string): string => input.replace(/\s+/g, ' ').trim();
1717
const varRegExp = (name: string): RegExp => new RegExp(`var \\w+ = \\[\"${name}\"\\];`);
1818

1919
const viewQueryRegExp = (descend: boolean, ref?: string): RegExp => {
20-
const maybeRef = ref ? `, ${ref}` : ``;
21-
return new RegExp(`i0\\.ɵviewQuery\\(\\w+, ${descend}${maybeRef}\\)`);
20+
const maybeRef = ref ? `${ref}` : `null`;
21+
return new RegExp(`i0\\.ɵviewQuery\\(\\w+, ${descend}, ${maybeRef}\\)`);
2222
};
2323

2424
const contentQueryRegExp = (predicate: string, descend: boolean, ref?: string): RegExp => {
25-
const maybeRef = ref ? `, ${ref}` : ``;
26-
return new RegExp(`i0\\.ɵcontentQuery\\(dirIndex, ${predicate}, ${descend}${maybeRef}\\)`);
25+
const maybeRef = ref ? `${ref}` : `null`;
26+
return new RegExp(`i0\\.ɵcontentQuery\\(dirIndex, ${predicate}, ${descend}, ${maybeRef}\\)`);
2727
};
2828

2929
describe('ngtsc behavioral tests', () => {
@@ -1017,7 +1017,7 @@ describe('ngtsc behavioral tests', () => {
10171017
expect(jsContents).toMatch(varRegExp('accessor'));
10181018
// match `i0.ɵcontentQuery(dirIndex, _c1, true, TemplateRef)`
10191019
expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef'));
1020-
// match `i0.ɵviewQuery(_c2, true)`
1020+
// match `i0.ɵviewQuery(_c2, true, null)`
10211021
expect(jsContents).toMatch(viewQueryRegExp(true));
10221022
});
10231023

@@ -1039,9 +1039,9 @@ describe('ngtsc behavioral tests', () => {
10391039

10401040
env.driveMain();
10411041
const jsContents = env.getContents('test.js');
1042-
// match `i0.ɵcontentQuery(dirIndex, TemplateRef, true)`
1042+
// match `i0.ɵcontentQuery(dirIndex, TemplateRef, true, null)`
10431043
expect(jsContents).toMatch(contentQueryRegExp('TemplateRef', true));
1044-
// match `i0.ɵcontentQuery(dirIndex, ViewContainerRef, true)`
1044+
// match `i0.ɵcontentQuery(dirIndex, ViewContainerRef, true, null)`
10451045
expect(jsContents).toMatch(contentQueryRegExp('ViewContainerRef', true));
10461046
});
10471047

packages/compiler/src/compiler_facade_interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export interface R3QueryMetadataFacade {
150150
predicate: any|string[];
151151
descendants: boolean;
152152
read: any|null;
153+
static: boolean;
153154
}
154155

155156
export interface ParseSourceSpan {

packages/compiler/src/jit_compiler_facade.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ function convertToR3QueryMetadata(facade: R3QueryMetadataFacade): R3QueryMetadat
199199
predicate: Array.isArray(facade.predicate) ? facade.predicate :
200200
new WrappedNodeExpr(facade.predicate),
201201
read: facade.read ? new WrappedNodeExpr(facade.read) : null,
202+
static: facade.static
202203
};
203204
}
204205

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export class Identifiers {
186186

187187
static queryRefresh: o.ExternalReference = {name: 'ɵqueryRefresh', moduleName: CORE};
188188
static viewQuery: o.ExternalReference = {name: 'ɵviewQuery', moduleName: CORE};
189+
static staticViewQuery: o.ExternalReference = {name: 'ɵstaticViewQuery', moduleName: CORE};
189190
static loadViewQuery: o.ExternalReference = {name: 'ɵloadViewQuery', moduleName: CORE};
190191
static contentQuery: o.ExternalReference = {name: 'ɵcontentQuery', moduleName: CORE};
191192
static loadContentQuery: o.ExternalReference = {name: 'ɵloadContentQuery', moduleName: CORE};

packages/compiler/src/render3/view/api.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,21 @@ export interface R3QueryMetadata {
229229
* for a given node is to be returned.
230230
*/
231231
read: o.Expression|null;
232+
233+
/**
234+
* Whether or not this query should collect only static results.
235+
*
236+
* If static is true, the query's results will be set on the component after nodes are created,
237+
* but before change detection runs. This means that any results that relied upon change detection
238+
* to run (e.g. results inside *ngIf or *ngFor views) will not be collected. Query results are
239+
* available in the ngOnInit hook.
240+
*
241+
* If static is false, the query's results will be set on the component after change detection
242+
* runs. This means that the query results can contain nodes inside *ngIf or *ngFor views, but
243+
* the results will not be available in the ngOnInit hook (only in the ngAfterContentInit for
244+
* content hooks and ngAfterViewInit for view hooks).
245+
*/
246+
static: boolean;
232247
}
233248

234249
/**

packages/compiler/src/render3/view/compiler.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ function queriesFromGlobalMetadata(
457457
first: query.first,
458458
predicate: selectorsFromGlobalMetadata(query.selectors, outputCtx),
459459
descendants: query.descendants, read,
460+
static: !!query.static
460461
};
461462
});
462463
}
@@ -490,10 +491,8 @@ function prepareQueryParams(query: R3QueryMetadata, constantPool: ConstantPool):
490491
const parameters = [
491492
getQueryPredicate(query, constantPool),
492493
o.literal(query.descendants),
494+
query.read || o.literal(null),
493495
];
494-
if (query.read) {
495-
parameters.push(query.read);
496-
}
497496
return parameters;
498497
}
499498

@@ -590,9 +589,11 @@ function createViewQueriesFunction(
590589
const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME);
591590

592591
meta.viewQueries.forEach((query: R3QueryMetadata) => {
592+
const queryInstruction = query.static ? R3.staticViewQuery : R3.viewQuery;
593+
593594
// creation, e.g. r3.viewQuery(somePredicate, true);
594595
const queryDefinition =
595-
o.importExpr(R3.viewQuery).callFn(prepareQueryParams(query, constantPool));
596+
o.importExpr(queryInstruction).callFn(prepareQueryParams(query, constantPool));
596597
createStatements.push(queryDefinition.toStmt());
597598

598599
// update, e.g. (r3.queryRefresh(tmp = r3.loadViewQuery()) && (ctx.someDir = tmp));

packages/core/src/compiler/compiler_facade_interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export interface R3QueryMetadataFacade {
150150
predicate: any|string[];
151151
descendants: boolean;
152152
read: any|null;
153+
static: boolean;
153154
}
154155

155156
export interface ParseSourceSpan {

packages/core/src/core_render3_private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export {
8181
containerRefreshEnd as ɵcontainerRefreshEnd,
8282
queryRefresh as ɵqueryRefresh,
8383
viewQuery as ɵviewQuery,
84+
staticViewQuery as ɵstaticViewQuery,
8485
loadViewQuery as ɵloadViewQuery,
8586
contentQuery as ɵcontentQuery,
8687
loadContentQuery as ɵloadContentQuery,

packages/core/src/render3/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export {
124124
export {
125125
queryRefresh,
126126
viewQuery,
127+
staticViewQuery,
127128
loadViewQuery,
128129
contentQuery,
129130
loadContentQuery,

packages/core/src/render3/instructions.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTEXT, DECLARATION_VIEW, Expa
3737
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
3838
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
3939
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
40-
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state';
40+
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode,} from './state';
4141
import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
4242
import {BoundPlayerFactory} from './styling/player_factory';
4343
import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput, hasStyling, isAnimationProp} from './styling/util';
@@ -784,6 +784,7 @@ export function createTView(
784784
expandoStartIndex: initialViewLength,
785785
expandoInstructions: null,
786786
firstTemplatePass: true,
787+
staticViewQueries: false,
787788
initHooks: null,
788789
checkHooks: null,
789790
contentHooks: null,
@@ -2922,20 +2923,23 @@ export function checkView<T>(hostView: LView, component: T) {
29222923

29232924
try {
29242925
namespaceHTML();
2925-
creationMode && executeViewQueryFn(hostView, hostTView, component);
2926+
creationMode && executeViewQueryFn(RenderFlags.Create, hostTView, component);
29262927
templateFn(getRenderFlags(hostView), component);
29272928
refreshDescendantViews(hostView);
2928-
!creationMode && executeViewQueryFn(hostView, hostTView, component);
2929+
// Only check view queries again in creation mode if there are static view queries
2930+
if (!creationMode || hostTView.staticViewQueries) {
2931+
executeViewQueryFn(RenderFlags.Update, hostTView, component);
2932+
}
29292933
} finally {
29302934
leaveView(oldView);
29312935
}
29322936
}
29332937

2934-
function executeViewQueryFn<T>(lView: LView, tView: TView, component: T): void {
2938+
function executeViewQueryFn<T>(flags: RenderFlags, tView: TView, component: T): void {
29352939
const viewQuery = tView.viewQuery;
29362940
if (viewQuery) {
29372941
setCurrentQueryIndex(tView.viewQueryStartIndex);
2938-
viewQuery(getRenderFlags(lView), component);
2942+
viewQuery(flags, component);
29392943
}
29402944
}
29412945

packages/core/src/render3/interfaces/view.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,14 @@ export interface TView {
376376
*/
377377
expandoStartIndex: number;
378378

379+
/**
380+
* Whether or not there are any static view queries tracked on this view.
381+
*
382+
* We store this so we know whether or not we should do a view query
383+
* refresh after creation mode to collect static query results.
384+
*/
385+
staticViewQueries: boolean;
386+
379387
/**
380388
* The index where the viewQueries section of `LView` begins. This section contains
381389
* view queries defined for a component/directive.

packages/core/src/render3/jit/directive.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ export function convertToR3QueryMetadata(propertyName: string, ann: Query): R3Qu
169169
predicate: convertToR3QueryPredicate(ann.selector),
170170
descendants: ann.descendants,
171171
first: ann.first,
172-
read: ann.read ? ann.read : null
172+
read: ann.read ? ann.read : null,
173+
static: !!ann.static
173174
};
174175
}
175176
function extractQueriesMetadata(

packages/core/src/render3/jit/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
8888
'ɵpipe': r3.pipe,
8989
'ɵqueryRefresh': r3.queryRefresh,
9090
'ɵviewQuery': r3.viewQuery,
91+
'ɵstaticViewQuery': r3.staticViewQuery,
9192
'ɵloadViewQuery': r3.loadViewQuery,
9293
'ɵcontentQuery': r3.contentQuery,
9394
'ɵloadContentQuery': r3.loadContentQuery,

0 commit comments

Comments
 (0)