fix(ivy): refresh child components before executing ViewQuery function (#32922)

Child component refresh must happen before executing the ViewQueryFn because
child components could insert a template from the host that contains the result
of the ViewQuery function (see related test added in this PR).

PR Close #32922
This commit is contained in:
Andrew Scott 2019-09-30 14:39:46 -07:00 committed by atscott
parent 088435c5eb
commit 72f3747d7b
2 changed files with 44 additions and 5 deletions

View File

@ -430,17 +430,20 @@ export function refreshView<T>(
setHostBindings(tView, lView); setHostBindings(tView, lView);
const viewQuery = tView.viewQuery;
if (viewQuery !== null) {
executeViewQueryFn(RenderFlags.Update, viewQuery, context);
}
// Refresh child component views. // Refresh child component views.
const components = tView.components; const components = tView.components;
if (components !== null) { if (components !== null) {
refreshChildComponents(lView, components); refreshChildComponents(lView, components);
} }
// View queries must execute after refreshing child components because a template in this view
// could be inserted in a child component. If the view query executes before child component
// refresh, the template might not yet be inserted.
const viewQuery = tView.viewQuery;
if (viewQuery !== null) {
executeViewQueryFn(RenderFlags.Update, viewQuery, context);
}
// execute view hooks (AfterViewInit, AfterViewChecked) // execute view hooks (AfterViewInit, AfterViewChecked)
// PERF WARNING: do NOT extract this to a separate function without running benchmarks // PERF WARNING: do NOT extract this to a separate function without running benchmarks
if (!checkNoChangesMode) { if (!checkNoChangesMode) {

View File

@ -265,6 +265,42 @@ describe('query logic', () => {
expect(fixture.componentInstance.foo.length).toBe(2); expect(fixture.componentInstance.foo.length).toBe(2);
}); });
it('should support ViewChild query where template is inserted in child component', () => {
@Component({selector: 'required', template: ''})
class Required {
}
@Component({
selector: 'insertion',
template: `<ng-container [ngTemplateOutlet]="content"></ng-container>`
})
class Insertion {
@Input() content !: TemplateRef<{}>;
}
@Component({
template: `
<ng-template #template>
<required></required>
</ng-template>
<insertion [content]="template"></insertion>
`
})
class App {
@ViewChild(Required, {static: false}) requiredEl !: Required;
viewChildAvailableInAfterViewInit?: boolean;
ngAfterViewInit() {
this.viewChildAvailableInAfterViewInit = this.requiredEl !== undefined;
}
}
const fixture = TestBed.configureTestingModule({declarations: [App, Insertion, Required]})
.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.viewChildAvailableInAfterViewInit).toBe(true);
});
}); });
describe('content queries', () => { describe('content queries', () => {