diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
index b28c67b354..757b034542 100644
--- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
+++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
@@ -1694,6 +1694,79 @@ describe('compiler compliance', () => {
expectEmit(source, ContentQueryComponentDefinition, 'Invalid ContentQuery declaration');
});
+ it('should support static content queries', () => {
+ const files = {
+ app: {
+ ...directive,
+ 'content_query.ts': `
+ import {Component, ContentChild, NgModule} from '@angular/core';
+ import {SomeDirective} from './some.directive';
+
+ @Component({
+ selector: 'content-query-component',
+ template: \`
+
+ \`
+ })
+ export class ContentQueryComponent {
+ @ContentChild(SomeDirective, {static: true}) someDir !: SomeDirective;
+ @ContentChild('foo', {static: false}) foo !: ElementRef;
+ }
+
+ @Component({
+ selector: 'my-app',
+ template: \`
+
+
+
+ \`
+ })
+ export class MyApp { }
+
+ @NgModule({declarations: [SomeDirective, ContentQueryComponent, MyApp]})
+ export class MyModule { }
+ `
+ }
+ };
+
+ const ContentQueryComponentDefinition = `
+ ContentQueryComponent.ngComponentDef = $r3$.ɵdefineComponent({
+ type: ContentQueryComponent,
+ selectors: [["content-query-component"]],
+ factory: function ContentQueryComponent_Factory(t) {
+ return new (t || ContentQueryComponent)();
+ },
+ contentQueries: function ContentQueryComponent_ContentQueries(rf, ctx, dirIndex) {
+ if (rf & 1) {
+ $r3$.ɵstaticContentQuery(dirIndex, SomeDirective, true, null);
+ $r3$.ɵcontentQuery(dirIndex, $ref0$, true, null);
+ }
+ if (rf & 2) {
+ var $tmp$;
+ ($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadContentQuery())) && (ctx.someDir = $tmp$.first));
+ ($r3$.ɵqueryRefresh(($tmp$ = $r3$.ɵloadContentQuery())) && (ctx.foo = $tmp$.first));
+ }
+ },
+ ngContentSelectors: $_c1$,
+ consts: 2,
+ vars: 0,
+ template: function ContentQueryComponent_Template(rf, ctx) {
+ if (rf & 1) {
+ $r3$.ɵprojectionDef();
+ $r3$.ɵelementStart(0, "div");
+ $r3$.ɵprojection(1);
+ $r3$.ɵelementEnd();
+ }
+ },
+ encapsulation: 2
+ });`;
+
+ const result = compile(files, angularFiles);
+ const source = result.source;
+
+ expectEmit(source, ContentQueryComponentDefinition, 'Invalid ContentQuery declaration');
+ });
+
it('should support content queries with read tokens specified', () => {
const files = {
app: {
diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts
index 40e7ee30c7..4ffac6f60c 100644
--- a/packages/compiler/src/render3/r3_identifiers.ts
+++ b/packages/compiler/src/render3/r3_identifiers.ts
@@ -187,6 +187,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 staticContentQuery: o.ExternalReference = {name: 'ɵstaticContentQuery', 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};
diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts
index 25ec289b01..782938fcb9 100644
--- a/packages/compiler/src/render3/view/compiler.ts
+++ b/packages/compiler/src/render3/view/compiler.ts
@@ -518,9 +518,12 @@ function createContentQueriesFunction(
const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME);
for (const query of meta.queries) {
- // creation, e.g. r3.contentQuery(dirIndex, somePredicate, true);
+ // creation, e.g. r3.contentQuery(dirIndex, somePredicate, true, null);
const args = [o.variable('dirIndex'), ...prepareQueryParams(query, constantPool) as any];
- createStatements.push(o.importExpr(R3.contentQuery).callFn(args).toStmt());
+
+ const queryInstruction = query.static ? R3.staticContentQuery : R3.contentQuery;
+
+ createStatements.push(o.importExpr(queryInstruction).callFn(args).toStmt());
// update, e.g. (r3.queryRefresh(tmp = r3.loadContentQuery()) && (ctx.someDir = tmp));
const temporary = tempAllocator();
diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts
index fd88d0ba06..3addbc8234 100644
--- a/packages/core/src/core_render3_private_export.ts
+++ b/packages/core/src/core_render3_private_export.ts
@@ -82,6 +82,7 @@ export {
queryRefresh as ɵqueryRefresh,
viewQuery as ɵviewQuery,
staticViewQuery as ɵstaticViewQuery,
+ staticContentQuery as ɵstaticContentQuery,
loadViewQuery as ɵloadViewQuery,
contentQuery as ɵcontentQuery,
loadContentQuery as ɵloadContentQuery,
diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts
index e525473d46..3b415f02a4 100644
--- a/packages/core/src/render3/index.ts
+++ b/packages/core/src/render3/index.ts
@@ -128,6 +128,7 @@ export {
loadViewQuery,
contentQuery,
loadContentQuery,
+ staticContentQuery
} from './query';
export {
diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts
index 1a350e7469..76576b8045 100644
--- a/packages/core/src/render3/instructions.ts
+++ b/packages/core/src/render3/instructions.ts
@@ -65,6 +65,8 @@ const enum BindingDirection {
*/
export function refreshDescendantViews(lView: LView) {
const tView = lView[TVIEW];
+ const creationMode = isCreationMode(lView);
+
// This needs to be set before children are processed to support recursive components
tView.firstTemplatePass = false;
@@ -73,7 +75,7 @@ export function refreshDescendantViews(lView: LView) {
// If this is a creation pass, we should not call lifecycle hooks or evaluate bindings.
// This will be done in the update pass.
- if (!isCreationMode(lView)) {
+ if (!creationMode) {
const checkNoChangesMode = getCheckNoChangesMode();
executeInitHooks(lView, tView, checkNoChangesMode);
@@ -90,6 +92,13 @@ export function refreshDescendantViews(lView: LView) {
setHostBindings(tView, lView);
}
+ // We resolve content queries specifically marked as `static` in creation mode. Dynamic
+ // content queries are resolved during change detection (i.e. update mode), after embedded
+ // views are refreshed (see block above).
+ if (creationMode && tView.staticContentQueries) {
+ refreshContentQueries(tView, lView);
+ }
+
refreshChildComponents(tView.components);
}
@@ -785,6 +794,7 @@ export function createTView(
expandoInstructions: null,
firstTemplatePass: true,
staticViewQueries: false,
+ staticContentQueries: false,
initHooks: null,
checkHooks: null,
contentHooks: null,
diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts
index 51c35ee782..589f48a44e 100644
--- a/packages/core/src/render3/interfaces/view.ts
+++ b/packages/core/src/render3/interfaces/view.ts
@@ -384,6 +384,14 @@ export interface TView {
*/
staticViewQueries: boolean;
+ /**
+ * Whether or not there are any static content queries tracked on this view.
+ *
+ * We store this so we know whether or not we should do a content query
+ * refresh after creation mode to collect static query results.
+ */
+ staticContentQueries: boolean;
+
/**
* The index where the viewQueries section of `LView` begins. This section contains
* view queries defined for a component/directive.
diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts
index f48e1f779b..b99c4a112d 100644
--- a/packages/core/src/render3/jit/environment.ts
+++ b/packages/core/src/render3/jit/environment.ts
@@ -89,6 +89,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵqueryRefresh': r3.queryRefresh,
'ɵviewQuery': r3.viewQuery,
'ɵstaticViewQuery': r3.staticViewQuery,
+ 'ɵstaticContentQuery': r3.staticContentQuery,
'ɵloadViewQuery': r3.loadViewQuery,
'ɵcontentQuery': r3.contentQuery,
'ɵloadContentQuery': r3.loadContentQuery,
diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts
index e13cc22692..5ec63a9824 100644
--- a/packages/core/src/render3/query.ts
+++ b/packages/core/src/render3/query.ts
@@ -442,7 +442,7 @@ export function loadViewQuery(): T {
*/
export function contentQuery(
directiveIndex: number, predicate: Type| string[], descend: boolean,
- // TODO: "read" should be an AbstractType (FW-486)
+ // TODO(FW-486): "read" should be an AbstractType
read: any): QueryList {
const lView = getLView();
const tView = lView[TVIEW];
@@ -459,6 +459,28 @@ export function contentQuery(
return contentQuery;
}
+/**
+ * Registers a QueryList, associated with a static content query, for later refresh
+ * (part of a view refresh).
+ *
+ * @param directiveIndex Current directive index
+ * @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
+ * @returns QueryList
+ */
+export function staticContentQuery(
+ directiveIndex: number, predicate: Type| string[], descend: boolean,
+ // TODO(FW-486): "read" should be an AbstractType
+ read: any): void {
+ const queryList = contentQuery(directiveIndex, predicate, descend, read) as QueryList_;
+ const tView = getLView()[TVIEW];
+ queryList._static = true;
+ if (!tView.staticContentQueries) {
+ tView.staticContentQueries = true;
+ }
+}
+
export function loadContentQuery(): QueryList {
const lView = getLView();
ngDevMode &&
diff --git a/packages/core/test/acceptance/query_spec.ts b/packages/core/test/acceptance/query_spec.ts
index 58633ed3ab..758b9ebcb9 100644
--- a/packages/core/test/acceptance/query_spec.ts
+++ b/packages/core/test/acceptance/query_spec.ts
@@ -9,7 +9,7 @@
import {Component, ContentChild, ContentChildren, Directive, ElementRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
-import {fixmeIvy, onlyInIvy} from '@angular/private/testing';
+import {onlyInIvy} from '@angular/private/testing';
describe('query logic', () => {
beforeEach(() => {
@@ -17,7 +17,7 @@ describe('query logic', () => {
declarations: [
AppComp, QueryComp, SimpleCompA, SimpleCompB, StaticViewQueryComp, TextDirective,
SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp,
- QueryCompWithChanges
+ QueryCompWithChanges, StaticContentQueryDir
]
});
});
@@ -256,41 +256,37 @@ describe('query logic', () => {
expect(comp.contentChildren.length).toBe(2);
});
- });
-
- fixmeIvy('Must support static content queries in Ivy')
- .it('should set static content child queries in creation mode (and just in creation mode)',
- () => {
- const template = `
+ it('should set static content child queries in creation mode (and just in creation mode)',
+ () => {
+ const template = `
`;
- TestBed.overrideComponent(AppComp, {set: new Component({template})});
- const fixture = TestBed.createComponent(AppComp);
- const component = fixture.debugElement.children[0].injector.get(StaticContentQueryComp);
+ TestBed.overrideComponent(AppComp, {set: new Component({template})});
+ const fixture = TestBed.createComponent(AppComp);
+ const component = fixture.debugElement.children[0].injector.get(StaticContentQueryComp);
- // static ContentChild 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 ContentChild 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 ContentChild query should not have been resolved yet
- expect(component.foo).not.toBeDefined();
+ // dynamic ContentChild query should not have been resolved yet
+ expect(component.foo).not.toBeDefined();
- const span = fixture.nativeElement.querySelector('span');
- (fixture.componentInstance as any).text = 'some text';
- fixture.detectChanges();
+ const span = fixture.nativeElement.querySelector('span');
+ (fixture.componentInstance as any).text = 'some text';
+ fixture.detectChanges();
- expect(component.textDir.text).toEqual('some text');
- expect(component.foo.nativeElement).toBe(span);
- expect(component.setEvents).toEqual(['textDir set', 'foo set']);
- });
+ 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 content queries in Ivy')
- .it('should support static content child queries inherited from superclasses', () => {
- const template = `
+ it('should support static content child queries inherited from superclasses', () => {
+ const template = `
@@ -298,28 +294,100 @@ describe('query logic', () => {
`;
- TestBed.overrideComponent(AppComp, {set: new Component({template})});
- const fixture = TestBed.createComponent(AppComp);
- const component =
- fixture.debugElement.children[0].injector.get(SubclassStaticContentQueryComp);
- const divs = fixture.nativeElement.querySelectorAll('div');
- const spans = fixture.nativeElement.querySelectorAll('span');
+ TestBed.overrideComponent(AppComp, {set: new Component({template})});
+ const fixture = TestBed.createComponent(AppComp);
+ const component =
+ fixture.debugElement.children[0].injector.get(SubclassStaticContentQueryComp);
+ const divs = fixture.nativeElement.querySelectorAll('div');
+ const spans = fixture.nativeElement.querySelectorAll('span');
- // static ContentChild 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 ContentChild 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 ContentChild queries should not have been resolved yet
- expect(component.foo).not.toBeDefined();
- expect(component.baz).not.toBeDefined();
+ // dynamic ContentChild queries should not have been resolved yet
+ expect(component.foo).not.toBeDefined();
+ expect(component.baz).not.toBeDefined();
- (fixture.componentInstance as any).text = 'some text';
- fixture.detectChanges();
- expect(component.textDir.text).toEqual('some text');
- expect(component.foo.nativeElement).toBe(spans[0]);
- expect(component.baz.nativeElement).toBe(spans[1]);
- });
+ (fixture.componentInstance as any).text = 'some text';
+ 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 set static content child queries on directives', () => {
+ const template = `
+
+ `;
+ TestBed.overrideComponent(AppComp, {set: new Component({template})});
+ const fixture = TestBed.createComponent(AppComp);
+ const component = fixture.debugElement.children[0].injector.get(StaticContentQueryDir);
+
+ // static ContentChild 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 ContentChild query should not have been resolved yet
+ expect(component.foo).not.toBeDefined();
+
+ const span = fixture.nativeElement.querySelector('span');
+ (fixture.componentInstance as any).text = 'some text';
+ fixture.detectChanges();
+
+ expect(component.textDir.text).toEqual('some text');
+ expect(component.foo.nativeElement).toBe(span);
+ expect(component.setEvents).toEqual(['textDir set', 'foo set']);
+ });
+
+ it('should support multiple content query components (multiple template passes)', () => {
+ const template = `
+
+
+
+
+
+
+
+
+ `;
+ TestBed.overrideComponent(AppComp, {set: new Component({template})});
+ const fixture = TestBed.createComponent(AppComp);
+ const firstComponent = fixture.debugElement.children[0].injector.get(StaticContentQueryComp);
+ const secondComponent = fixture.debugElement.children[1].injector.get(StaticContentQueryComp);
+
+ // static ContentChild 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 ContentChild query should not have been resolved yet
+ expect(firstComponent.foo).not.toBeDefined();
+ expect(secondComponent.foo).not.toBeDefined();
+
+ const spans = fixture.nativeElement.querySelectorAll('span');
+ (fixture.componentInstance as any).text = 'some text';
+ 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']);
+ });
+
+ });
describe('observable interface', () => {
@@ -453,6 +521,29 @@ class StaticContentQueryComp {
}
}
+@Directive({selector: '[staticContentQueryDir]'})
+class StaticContentQueryDir {
+ private _textDir !: TextDirective;
+ private _foo !: ElementRef;
+ setEvents: string[] = [];
+
+ @ContentChild(TextDirective, {static: true})
+ get textDir(): TextDirective { return this._textDir; }
+
+ set textDir(value: TextDirective) {
+ this.setEvents.push('textDir set');
+ this._textDir = value;
+ }
+
+ @ContentChild('foo', {static: false})
+ get foo(): ElementRef { return this._foo; }
+
+ set foo(value: ElementRef) {
+ this.setEvents.push('foo set');
+ this._foo = value;
+ }
+}
+
@Component({selector: 'subclass-static-content-query-comp', template: ``})
class SubclassStaticContentQueryComp extends StaticContentQueryComp {
@ContentChild('bar', {static: true})