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

This commit adds support for the `static: true` flag in `ContentChild`
queries. Prior to this commit, all `ContentChild` 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
@ContentChild(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 18:18:56 -08:00 committed by Igor Minar
parent a4638d5a81
commit 3c1a1620e3
10 changed files with 261 additions and 50 deletions

View File

@ -1694,6 +1694,79 @@ describe('compiler compliance', () => {
expectEmit(source, ContentQueryComponentDefinition, 'Invalid ContentQuery declaration'); 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: \`
<div><ng-content></ng-content></div>
\`
})
export class ContentQueryComponent {
@ContentChild(SomeDirective, {static: true}) someDir !: SomeDirective;
@ContentChild('foo', {static: false}) foo !: ElementRef;
}
@Component({
selector: 'my-app',
template: \`
<content-query-component>
<div someDir></div>
</content-query-component>
\`
})
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', () => { it('should support content queries with read tokens specified', () => {
const files = { const files = {
app: { app: {

View File

@ -187,6 +187,7 @@ export class Identifiers {
static queryRefresh: o.ExternalReference = {name: 'ɵqueryRefresh', moduleName: CORE}; static queryRefresh: o.ExternalReference = {name: 'ɵqueryRefresh', moduleName: CORE};
static viewQuery: o.ExternalReference = {name: 'ɵviewQuery', moduleName: CORE}; static viewQuery: o.ExternalReference = {name: 'ɵviewQuery', moduleName: CORE};
static staticViewQuery: o.ExternalReference = {name: 'ɵstaticViewQuery', 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 loadViewQuery: o.ExternalReference = {name: 'ɵloadViewQuery', moduleName: CORE};
static contentQuery: o.ExternalReference = {name: 'ɵcontentQuery', moduleName: CORE}; static contentQuery: o.ExternalReference = {name: 'ɵcontentQuery', moduleName: CORE};
static loadContentQuery: o.ExternalReference = {name: 'ɵloadContentQuery', moduleName: CORE}; static loadContentQuery: o.ExternalReference = {name: 'ɵloadContentQuery', moduleName: CORE};

View File

@ -518,9 +518,12 @@ function createContentQueriesFunction(
const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME); const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME);
for (const query of meta.queries) { 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]; 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)); // update, e.g. (r3.queryRefresh(tmp = r3.loadContentQuery()) && (ctx.someDir = tmp));
const temporary = tempAllocator(); const temporary = tempAllocator();

View File

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

View File

@ -128,6 +128,7 @@ export {
loadViewQuery, loadViewQuery,
contentQuery, contentQuery,
loadContentQuery, loadContentQuery,
staticContentQuery
} from './query'; } from './query';
export { export {

View File

@ -65,6 +65,8 @@ const enum BindingDirection {
*/ */
export function refreshDescendantViews(lView: LView) { export function refreshDescendantViews(lView: LView) {
const tView = lView[TVIEW]; const tView = lView[TVIEW];
const creationMode = isCreationMode(lView);
// This needs to be set before children are processed to support recursive components // This needs to be set before children are processed to support recursive components
tView.firstTemplatePass = false; 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. // If this is a creation pass, we should not call lifecycle hooks or evaluate bindings.
// This will be done in the update pass. // This will be done in the update pass.
if (!isCreationMode(lView)) { if (!creationMode) {
const checkNoChangesMode = getCheckNoChangesMode(); const checkNoChangesMode = getCheckNoChangesMode();
executeInitHooks(lView, tView, checkNoChangesMode); executeInitHooks(lView, tView, checkNoChangesMode);
@ -90,6 +92,13 @@ export function refreshDescendantViews(lView: LView) {
setHostBindings(tView, 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); refreshChildComponents(tView.components);
} }
@ -785,6 +794,7 @@ export function createTView(
expandoInstructions: null, expandoInstructions: null,
firstTemplatePass: true, firstTemplatePass: true,
staticViewQueries: false, staticViewQueries: false,
staticContentQueries: false,
initHooks: null, initHooks: null,
checkHooks: null, checkHooks: null,
contentHooks: null, contentHooks: null,

View File

@ -384,6 +384,14 @@ export interface TView {
*/ */
staticViewQueries: boolean; 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 * The index where the viewQueries section of `LView` begins. This section contains
* view queries defined for a component/directive. * view queries defined for a component/directive.

View File

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

View File

@ -442,7 +442,7 @@ export function loadViewQuery<T>(): T {
*/ */
export function contentQuery<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) // TODO(FW-486): "read" should be an AbstractType
read: any): QueryList<T> { read: any): QueryList<T> {
const lView = getLView(); const lView = getLView();
const tView = lView[TVIEW]; const tView = lView[TVIEW];
@ -459,6 +459,28 @@ export function contentQuery<T>(
return 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<T>
*/
export function staticContentQuery<T>(
directiveIndex: number, predicate: Type<any>| string[], descend: boolean,
// TODO(FW-486): "read" should be an AbstractType
read: any): void {
const queryList = contentQuery(directiveIndex, predicate, descend, read) as QueryList_<T>;
const tView = getLView()[TVIEW];
queryList._static = true;
if (!tView.staticContentQueries) {
tView.staticContentQueries = true;
}
}
export function loadContentQuery<T>(): QueryList<T> { export function loadContentQuery<T>(): QueryList<T> {
const lView = getLView(); const lView = getLView();
ngDevMode && ngDevMode &&

View File

@ -9,7 +9,7 @@
import {Component, ContentChild, ContentChildren, Directive, ElementRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren} from '@angular/core'; import {Component, ContentChild, ContentChildren, Directive, ElementRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers'; 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', () => { describe('query logic', () => {
beforeEach(() => { beforeEach(() => {
@ -17,7 +17,7 @@ describe('query logic', () => {
declarations: [ declarations: [
AppComp, QueryComp, SimpleCompA, SimpleCompB, StaticViewQueryComp, TextDirective, AppComp, QueryComp, SimpleCompA, SimpleCompB, StaticViewQueryComp, TextDirective,
SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp, SubclassStaticViewQueryComp, StaticContentQueryComp, SubclassStaticContentQueryComp,
QueryCompWithChanges QueryCompWithChanges, StaticContentQueryDir
] ]
}); });
}); });
@ -256,41 +256,37 @@ describe('query logic', () => {
expect(comp.contentChildren.length).toBe(2); expect(comp.contentChildren.length).toBe(2);
}); });
}); it('should set static content child queries in creation mode (and just in creation mode)',
() => {
fixmeIvy('Must support static content queries in Ivy') const template = `
.it('should set static content child queries in creation mode (and just in creation mode)',
() => {
const template = `
<static-content-query-comp> <static-content-query-comp>
<div [text]="text"></div> <div [text]="text"></div>
<span #foo></span> <span #foo></span>
</static-content-query-comp> </static-content-query-comp>
`; `;
TestBed.overrideComponent(AppComp, {set: new Component({template})}); TestBed.overrideComponent(AppComp, {set: new Component({template})});
const fixture = TestBed.createComponent(AppComp); const fixture = TestBed.createComponent(AppComp);
const component = fixture.debugElement.children[0].injector.get(StaticContentQueryComp); const component = fixture.debugElement.children[0].injector.get(StaticContentQueryComp);
// static ContentChild query should be set in creation mode, before CD runs // static ContentChild query should be set in creation mode, before CD runs
expect(component.textDir).toBeAnInstanceOf(TextDirective); expect(component.textDir).toBeAnInstanceOf(TextDirective);
expect(component.textDir.text).toEqual(''); expect(component.textDir.text).toEqual('');
expect(component.setEvents).toEqual(['textDir set']); expect(component.setEvents).toEqual(['textDir set']);
// dynamic ContentChild query should not have been resolved yet // dynamic ContentChild query should not have been resolved yet
expect(component.foo).not.toBeDefined(); expect(component.foo).not.toBeDefined();
const span = fixture.nativeElement.querySelector('span'); const span = fixture.nativeElement.querySelector('span');
(fixture.componentInstance as any).text = 'some text'; (fixture.componentInstance as any).text = 'some text';
fixture.detectChanges(); fixture.detectChanges();
expect(component.textDir.text).toEqual('some text'); expect(component.textDir.text).toEqual('some text');
expect(component.foo.nativeElement).toBe(span); expect(component.foo.nativeElement).toBe(span);
expect(component.setEvents).toEqual(['textDir set', 'foo set']); 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', () => {
.it('should support static content child queries inherited from superclasses', () => { const template = `
const template = `
<subclass-static-content-query-comp> <subclass-static-content-query-comp>
<div [text]="text"></div> <div [text]="text"></div>
<span #foo></span> <span #foo></span>
@ -298,28 +294,100 @@ describe('query logic', () => {
<span #baz></span> <span #baz></span>
</subclass-static-content-query-comp> </subclass-static-content-query-comp>
`; `;
TestBed.overrideComponent(AppComp, {set: new Component({template})}); TestBed.overrideComponent(AppComp, {set: new Component({template})});
const fixture = TestBed.createComponent(AppComp); const fixture = TestBed.createComponent(AppComp);
const component = const component =
fixture.debugElement.children[0].injector.get(SubclassStaticContentQueryComp); fixture.debugElement.children[0].injector.get(SubclassStaticContentQueryComp);
const divs = fixture.nativeElement.querySelectorAll('div'); const divs = fixture.nativeElement.querySelectorAll('div');
const spans = fixture.nativeElement.querySelectorAll('span'); const spans = fixture.nativeElement.querySelectorAll('span');
// static ContentChild queries should be set in creation mode, before CD runs // static ContentChild queries should be set in creation mode, before CD runs
expect(component.textDir).toBeAnInstanceOf(TextDirective); expect(component.textDir).toBeAnInstanceOf(TextDirective);
expect(component.textDir.text).toEqual(''); expect(component.textDir.text).toEqual('');
expect(component.bar.nativeElement).toEqual(divs[1]); expect(component.bar.nativeElement).toEqual(divs[1]);
// dynamic ContentChild queries should not have been resolved yet // dynamic ContentChild queries should not have been resolved yet
expect(component.foo).not.toBeDefined(); expect(component.foo).not.toBeDefined();
expect(component.baz).not.toBeDefined(); expect(component.baz).not.toBeDefined();
(fixture.componentInstance as any).text = 'some text'; (fixture.componentInstance as any).text = 'some text';
fixture.detectChanges(); fixture.detectChanges();
expect(component.textDir.text).toEqual('some text'); expect(component.textDir.text).toEqual('some text');
expect(component.foo.nativeElement).toBe(spans[0]); expect(component.foo.nativeElement).toBe(spans[0]);
expect(component.baz.nativeElement).toBe(spans[1]); expect(component.baz.nativeElement).toBe(spans[1]);
}); });
it('should set static content child queries on directives', () => {
const template = `
<div staticContentQueryDir>
<div [text]="text"></div>
<span #foo></span>
</div>
`;
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 = `
<static-content-query-comp>
<div [text]="text"></div>
<span #foo></span>
</static-content-query-comp>
<static-content-query-comp>
<div [text]="text"></div>
<span #foo></span>
</static-content-query-comp>
`;
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', () => { 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: `<ng-content></ng-content>`}) @Component({selector: 'subclass-static-content-query-comp', template: `<ng-content></ng-content>`})
class SubclassStaticContentQueryComp extends StaticContentQueryComp { class SubclassStaticContentQueryComp extends StaticContentQueryComp {
@ContentChild('bar', {static: true}) @ContentChild('bar', {static: true})