fix(ivy): allow view and content queries to match the same element (#24507)

When creating content queries from a directive on an element we need to take into account
existing view queries. The same element can be reported to both content and view queries
so freshly created content queries must be combined with pre-existing view queries.

PR Close #24507
This commit is contained in:
Pawel Kozlowski 2018-06-14 15:45:21 +02:00 committed by Miško Hevery
parent 7c8159b3e2
commit 6d246d6c72
2 changed files with 67 additions and 3 deletions

View File

@ -124,7 +124,7 @@ let currentQueries: LQueries|null;
export function getCurrentQueries(QueryType: {new (): LQueries}): LQueries {
// top level variables should not be exported for performance reasons (PERF_NOTES.md)
return currentQueries || (currentQueries = new QueryType());
return currentQueries || (currentQueries = (previousOrParentNode.queries || new QueryType()));
}
/**

View File

@ -7,12 +7,12 @@
*/
import {NgForOfContext} from '@angular/common';
import {TemplateRef, ViewContainerRef} from '@angular/core';
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
import {EventEmitter} from '../..';
import {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
import {AttributeMarker, QueryList, defineComponent, defineDirective, detectChanges, injectViewContainerRef} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, loadDirective} from '../../src/render3/instructions';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, loadDirective} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {query, queryRefresh} from '../../src/render3/query';
@ -1365,4 +1365,68 @@ describe('query', () => {
expect((queryInstance !.changes as EventEmitter<any>).closed).toBeTruthy();
});
});
describe('content', () => {
// https://stackblitz.com/edit/angular-wlenwd?file=src%2Fapp%2Fapp.component.ts
it('should support view and content queries matching the same element', () => {
let withContentComponentInstance: WithContentComponent;
class WithContentComponent {
// @ContentChildren('foo') foos;
foos: QueryList<ElementRef>;
static ngComponentDef = defineComponent({
type: WithContentComponent,
selectors: [['with-content']],
factory: () => {
return [new WithContentComponent(), query(null, ['foo'], true, QUERY_READ_FROM_NODE)];
},
template: (rf: RenderFlags, ctx: WithContentComponent) => {
// intentionally left empty, don't need anything for this test
},
hostBindings: function ContentQueryComponent_HostBindings(
dirIndex: number, elIndex: number) {
let tmp: any;
withContentComponentInstance = loadDirective<any[]>(dirIndex)[0];
queryRefresh(tmp = loadDirective<any[]>(dirIndex)[1]) &&
(withContentComponentInstance.foos = tmp as QueryList<any>);
},
});
}
/**
* <with-content>
* <div #foo></div>
* </with-content>
* <div id="after" #bar></div>
* class Cmpt {
* @ViewChildren('foo, bar') foos;
* }
*/
const AppComponent = createComponent('app-component', function(rf: RenderFlags, ctx: any) {
let tmp: any;
if (rf & RenderFlags.Create) {
query(0, ['foo', 'bar'], true, QUERY_READ_FROM_NODE);
elementStart(1, 'with-content');
{ element(2, 'div', null, ['foo', '']); }
elementEnd();
element(4, 'div', ['id', 'after'], ['bar', '']);
}
if (rf & RenderFlags.Update) {
queryRefresh(tmp = load<QueryList<any>>(0)) && (ctx.foos = tmp as QueryList<any>);
}
}, [WithContentComponent]);
const fixture = new ComponentFixture(AppComponent);
const viewQList = fixture.component.foos;
expect(viewQList.length).toBe(2);
expect(withContentComponentInstance !.foos.length).toBe(1);
expect(viewQList.first.nativeElement)
.toBe(withContentComponentInstance !.foos.first.nativeElement);
expect(viewQList.last.nativeElement.id).toBe('after');
});
});
});