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:
parent
a4638d5a81
commit
3c1a1620e3
|
@ -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: \`
|
||||
<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', () => {
|
||||
const files = {
|
||||
app: {
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -128,6 +128,7 @@ export {
|
|||
loadViewQuery,
|
||||
contentQuery,
|
||||
loadContentQuery,
|
||||
staticContentQuery
|
||||
} from './query';
|
||||
|
||||
export {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -442,7 +442,7 @@ export function loadViewQuery<T>(): T {
|
|||
*/
|
||||
export function contentQuery<T>(
|
||||
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> {
|
||||
const lView = getLView();
|
||||
const tView = lView[TVIEW];
|
||||
|
@ -459,6 +459,28 @@ export function contentQuery<T>(
|
|||
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> {
|
||||
const lView = getLView();
|
||||
ngDevMode &&
|
||||
|
|
|
@ -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 = `
|
||||
<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 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 = `
|
||||
<subclass-static-content-query-comp>
|
||||
<div [text]="text"></div>
|
||||
<span #foo></span>
|
||||
|
@ -298,28 +294,100 @@ describe('query logic', () => {
|
|||
<span #baz></span>
|
||||
</subclass-static-content-query-comp>
|
||||
`;
|
||||
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 = `
|
||||
<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', () => {
|
||||
|
||||
|
@ -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>`})
|
||||
class SubclassStaticContentQueryComp extends StaticContentQueryComp {
|
||||
@ContentChild('bar', {static: true})
|
||||
|
|
Loading…
Reference in New Issue