fix(ivy): Content Queries inheritance fix (#28324)

Prior to this change contentQueriesRefresh functions that represent refresh logic for @ContentQuery list were not composable, which caused problems in case one Directive inherits another one and both of them contain Content Queries. Due to the fact that we used indices to reference queries in refresh function, results were placed into wrong Queries. In order to avoid that we no longer use indices to reference queries and instead maintain current content query index while iterating through them. This allows us to compose contentQueriesRefresh functions and make inheritance feature work with Content Queries.

PR Close #28324
This commit is contained in:
Andrew Kushnir
2019-01-23 11:54:43 -08:00
committed by Jason Aden
parent ebac5dba38
commit bb94434d85
19 changed files with 224 additions and 190 deletions

View File

@ -9,8 +9,8 @@
import {ElementRef, QueryList, ViewContainerRef} from '@angular/core';
import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature} from '../../src/render3/index';
import {allocHostVars, bind, directiveInject, element, elementAttribute, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions';
import {query, queryRefresh} from '../../src/render3/query';
import {allocHostVars, bind, directiveInject, element, elementAttribute, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, elementHostAttrs} from '../../src/render3/instructions';
import {loadContentQuery, contentQuery, queryRefresh} from '../../src/render3/query';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {pureFunction1, pureFunction2} from '../../src/render3/pure_function';
import {sanitizeUrl, sanitizeUrlOrResourceUrl, sanitizeHtml} from '../../src/sanitization/sanitization';
@ -1009,11 +1009,11 @@ describe('host bindings', () => {
elementProperty(elIndex, 'id', bind(ctx.foos.length), null, true);
}
},
contentQueries: (dirIndex) => { registerContentQuery(query(['foo']), dirIndex); },
contentQueriesRefresh: (dirIndex: number, queryStartIdx: number) => {
contentQueries: (dirIndex: number) => { contentQuery(dirIndex, ['foo']); },
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
const instance = load<HostBindingWithContentChildren>(dirIndex);
queryRefresh(tmp = loadQueryList<ElementRef>(queryStartIdx)) && (instance.foos = tmp);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (instance.foos = tmp);
},
template: (rf: RenderFlags, cmp: HostBindingWithContentChildren) => {}
});

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, InjectionToken, QueryList} from '../../src/core';
import {ComponentDef, DirectiveDef, InheritDefinitionFeature, NgOnChangesFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty, loadViewQuery, queryRefresh, viewQuery} from '../../src/render3/index';
import {ElementRef, Inject, InjectionToken, QueryList, ɵAttributeMarker as AttributeMarker} from '../../src/core';
import {ComponentDef, DirectiveDef, InheritDefinitionFeature, NgOnChangesFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, contentQuery, defineBase, defineComponent, defineDirective, directiveInject, element, elementEnd, elementProperty, elementStart, load, loadContentQuery, loadViewQuery, queryRefresh, viewQuery} from '../../src/render3/index';
import {ComponentFixture, createComponent, getDirectiveOnNode} from './render_util';
@ -549,14 +549,14 @@ describe('InheritDefinitionFeature', () => {
});
it('should compose contentQueriesRefresh', () => {
const log: Array<[string, number, number]> = [];
const log: Array<[string, number]> = [];
class SuperDirective {
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
contentQueriesRefresh: (directiveIndex: number, queryIndex: number) => {
log.push(['super', directiveIndex, queryIndex]);
contentQueriesRefresh: (directiveIndex: number) => {
log.push(['super', directiveIndex]);
},
factory: () => new SuperDirective(),
});
@ -566,8 +566,8 @@ describe('InheritDefinitionFeature', () => {
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
contentQueriesRefresh: (directiveIndex: number, queryIndex: number) => {
log.push(['sub', directiveIndex, queryIndex]);
contentQueriesRefresh: (directiveIndex: number) => {
log.push(['sub', directiveIndex]);
},
factory: () => new SubDirective(),
features: [InheritDefinitionFeature]
@ -576,9 +576,68 @@ describe('InheritDefinitionFeature', () => {
const subDef = SubDirective.ngDirectiveDef as DirectiveDef<any>;
subDef.contentQueriesRefresh !(1, 2);
subDef.contentQueriesRefresh !(1);
expect(log).toEqual([['super', 1, 2], ['sub', 1, 2]]);
expect(log).toEqual([['super', 1], ['sub', 1]]);
});
it('should compose contentQueries and contentQueriesRefresh', () => {
let dirInstance: SubDirective;
class SuperDirective {
// @ContentChildren('foo')
foos !: QueryList<ElementRef>;
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'super-dir', '']],
factory: () => new SuperDirective(),
contentQueries: (dirIndex: number) => { contentQuery(dirIndex, ['foo'], true); },
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
const instance = load<SuperDirective>(dirIndex);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (instance.foos = tmp);
}
});
}
class SubDirective extends SuperDirective {
// @ContentChildren('bar')
bars !: QueryList<ElementRef>;
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'sub-dir', '']],
factory: () => new SubDirective(),
contentQueries: (dirIndex: number) => { contentQuery(dirIndex, ['bar'], true); },
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
dirInstance = load<SubDirective>(dirIndex);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (dirInstance.bars = tmp);
},
features: [InheritDefinitionFeature]
});
}
/**
* <div sub-dir>
* <span #foo></span>
* <span #bar></span>
* </div>
*/
const AppComponent = createComponent('app-component', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div', [AttributeMarker.SelectOnly, 'sub-dir']);
{
element(1, 'span', null, ['foo', '']);
element(3, 'span', null, ['bar', '']);
}
elementEnd();
}
}, 5, 0, [SubDirective]);
const fixture = new ComponentFixture(AppComponent);
expect(dirInstance !.foos.length).toBe(1);
expect(dirInstance !.bars.length).toBe(1);
});
it('should throw if inheriting a component from a directive', () => {

View File

@ -13,9 +13,9 @@ import {EventEmitter} from '../..';
import {AttributeMarker, ProvidersFeature, defineComponent, defineDirective, detectChanges} from '../../src/render3/index';
import {getNativeByIndex} from '../../src/render3/util';
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, loadQueryList, reference, registerContentQuery, template, text} from '../../src/render3/instructions';
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, reference, template, text} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {query, queryRefresh, viewQuery, loadViewQuery} from '../../src/render3/query';
import {queryRefresh, viewQuery, loadViewQuery, contentQuery, loadContentQuery} from '../../src/render3/query';
import {getLView} from '../../src/render3/state';
import {templateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound';
@ -2282,12 +2282,11 @@ describe('query', () => {
type: WithContentDirective,
selectors: [['', 'with-content', '']],
factory: () => new WithContentDirective(),
contentQueries: (dirIndex) => { registerContentQuery(query(['foo'], true), dirIndex); },
contentQueriesRefresh: (dirIndex: number, queryStartIdx: number) => {
contentQueries: (dirIndex: number) => { contentQuery(dirIndex, ['foo'], true); },
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
withContentInstance = load<WithContentDirective>(dirIndex);
queryRefresh(tmp = loadQueryList<ElementRef>(queryStartIdx)) &&
(withContentInstance.foos = tmp);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (withContentInstance.foos = tmp);
}
});
}
@ -2303,12 +2302,11 @@ describe('query', () => {
template: function(rf: RenderFlags, ctx: any) {},
consts: 0,
vars: 0,
contentQueries: (dirIndex) => { registerContentQuery(query(['foo'], false), dirIndex); },
contentQueriesRefresh: (dirIndex: number, queryStartIdx: number) => {
contentQueries: (dirIndex: number) => { contentQuery(dirIndex, ['foo'], false); },
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
shallowCompInstance = load<ShallowComp>(dirIndex);
queryRefresh(tmp = loadQueryList<ElementRef>(queryStartIdx)) &&
(shallowCompInstance.foos = tmp);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (shallowCompInstance.foos = tmp);
}
});
}
@ -2527,16 +2525,15 @@ describe('query', () => {
selectors: [['', 'query', '']],
exportAs: ['query'],
factory: () => new QueryDirective(),
contentQueries: (dirIndex) => {
contentQueries: (dirIndex: number) => {
// @ContentChildren('foo, bar, baz', {descendants: true}) fooBars:
// QueryList<ElementRef>;
registerContentQuery(query(['foo', 'bar', 'baz'], true), dirIndex);
contentQuery(dirIndex, ['foo', 'bar', 'baz'], true);
},
contentQueriesRefresh: (dirIndex: number, queryStartIdx: number) => {
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
const instance = load<QueryDirective>(dirIndex);
queryRefresh(tmp = loadQueryList<ElementRef>(queryStartIdx)) &&
(instance.fooBars = tmp);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (instance.fooBars = tmp);
},
});
}
@ -2591,16 +2588,15 @@ describe('query', () => {
selectors: [['', 'query', '']],
exportAs: ['query'],
factory: () => new QueryDirective(),
contentQueries: (dirIndex) => {
contentQueries: (dirIndex: number) => {
// @ContentChildren('foo, bar, baz', {descendants: true}) fooBars:
// QueryList<ElementRef>;
registerContentQuery(query(['foo'], false), dirIndex);
contentQuery(dirIndex, ['foo'], false);
},
contentQueriesRefresh: (dirIndex: number, queryStartIdx: number) => {
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
const instance = load<QueryDirective>(dirIndex);
queryRefresh(tmp = loadQueryList<ElementRef>(queryStartIdx)) &&
(instance.fooBars = tmp);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (instance.fooBars = tmp);
},
});
}
@ -2647,16 +2643,15 @@ describe('query', () => {
selectors: [['', 'query', '']],
exportAs: ['query'],
factory: () => new QueryDirective(),
contentQueries: (dirIndex) => {
contentQueries: (dirIndex: number) => {
// @ContentChildren('foo', {descendants: true}) fooBars:
// QueryList<ElementRef>;
registerContentQuery(query(['foo'], false), dirIndex);
contentQuery(dirIndex, ['foo'], false);
},
contentQueriesRefresh: (dirIndex: number, queryStartIdx: number) => {
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
const instance = load<QueryDirective>(dirIndex);
queryRefresh(tmp = loadQueryList<ElementRef>(queryStartIdx)) &&
(instance.fooBars = tmp);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (instance.fooBars = tmp);
},
});
}
@ -2703,15 +2698,14 @@ describe('query', () => {
selectors: [['', 'shallow-query', '']],
exportAs: ['shallow-query'],
factory: () => new ShallowQueryDirective(),
contentQueries: (dirIndex) => {
contentQueries: (dirIndex: number) => {
// @ContentChildren('foo', {descendants: false}) foos: QueryList<ElementRef>;
registerContentQuery(query(['foo'], false), dirIndex);
contentQuery(dirIndex, ['foo'], false);
},
contentQueriesRefresh: (dirIndex: number, queryStartIdx: number) => {
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
const instance = load<ShallowQueryDirective>(dirIndex);
queryRefresh(tmp = loadQueryList<ElementRef>(queryStartIdx)) &&
(instance.foos = tmp);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (instance.foos = tmp);
},
});
}
@ -2723,15 +2717,14 @@ describe('query', () => {
selectors: [['', 'deep-query', '']],
exportAs: ['deep-query'],
factory: () => new DeepQueryDirective(),
contentQueries: (dirIndex) => {
contentQueries: (dirIndex: number) => {
// @ContentChildren('foo', {descendants: false}) foos: QueryList<ElementRef>;
registerContentQuery(query(['foo'], true), dirIndex);
contentQuery(dirIndex, ['foo'], true);
},
contentQueriesRefresh: (dirIndex: number, queryStartIdx: number) => {
contentQueriesRefresh: (dirIndex: number) => {
let tmp: any;
const instance = load<DeepQueryDirective>(dirIndex);
queryRefresh(tmp = loadQueryList<ElementRef>(queryStartIdx)) &&
(instance.foos = tmp);
queryRefresh(tmp = loadContentQuery<ElementRef>()) && (instance.foos = tmp);
},
});
}