fix(ivy): queries not being inherited from undecorated classes (#30015)

Fixes view and content queries not being inherited in Ivy, if the base class hasn't been annotated with an Angular decorator (e.g. `Component` or `Directive`).

Also reworks the way the `ngBaseDef` is created so that it is added at the same point as the queries, rather than inside of the `Input` and `Output` decorators.

This PR partially resolves FW-1275. Support for host bindings will be added in a follow-up, because this PR is somewhat large as it is.

PR Close #30015
This commit is contained in:
Kristiyan Kostadinov
2019-04-21 17:37:15 +02:00
committed by Andrew Kushnir
parent 8ca208ff59
commit c7f1b0a97f
15 changed files with 688 additions and 140 deletions

View File

@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {R3BaseRefMetaData, compileBaseDefFromMetadata} from '@angular/compiler';
import {ConstantPool, R3BaseRefMetaData, compileBaseDefFromMetadata} from '@angular/compiler';
import {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, ClassMember, Decorator, ReflectionHost} from '../../reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform';
import {queriesFromFields} from './directive';
import {isAngularDecorator} from './util';
function containsNgTopLevelDecorator(decorators: Decorator[] | null, isCore: boolean): boolean {
@ -44,17 +45,30 @@ export class BaseDefDecoratorHandler implements
this.reflector.getMembersOfClass(node).forEach(property => {
const {decorators} = property;
if (decorators) {
for (const decorator of decorators) {
if (isAngularDecorator(decorator, 'Input', this.isCore)) {
result = result || {};
const inputs = result.inputs = result.inputs || [];
inputs.push({decorator, property});
} else if (isAngularDecorator(decorator, 'Output', this.isCore)) {
result = result || {};
const outputs = result.outputs = result.outputs || [];
outputs.push({decorator, property});
}
if (!decorators) {
return;
}
for (const decorator of decorators) {
if (isAngularDecorator(decorator, 'Input', this.isCore)) {
result = result || {};
const inputs = result.inputs = result.inputs || [];
inputs.push({decorator, property});
} else if (isAngularDecorator(decorator, 'Output', this.isCore)) {
result = result || {};
const outputs = result.outputs = result.outputs || [];
outputs.push({decorator, property});
} else if (
isAngularDecorator(decorator, 'ViewChild', this.isCore) ||
isAngularDecorator(decorator, 'ViewChildren', this.isCore)) {
result = result || {};
const viewQueries = result.viewQueries = result.viewQueries || [];
viewQueries.push({member: property, decorators});
} else if (
isAngularDecorator(decorator, 'ContentChild', this.isCore) ||
isAngularDecorator(decorator, 'ContentChildren', this.isCore)) {
result = result || {};
const queries = result.queries = result.queries || [];
queries.push({member: property, decorators});
}
}
});
@ -110,11 +124,21 @@ export class BaseDefDecoratorHandler implements
});
}
if (metadata.viewQueries) {
analysis.viewQueries =
queriesFromFields(metadata.viewQueries, this.reflector, this.evaluator);
}
if (metadata.queries) {
analysis.queries = queriesFromFields(metadata.queries, this.reflector, this.evaluator);
}
return {analysis};
}
compile(node: ClassDeclaration, analysis: R3BaseRefMetaData): CompileResult[]|CompileResult {
const {expression, type} = compileBaseDefFromMetadata(analysis);
compile(node: ClassDeclaration, analysis: R3BaseRefMetaData, pool: ConstantPool):
CompileResult[]|CompileResult {
const {expression, type} = compileBaseDefFromMetadata(analysis, pool);
return {
name: 'ngBaseDef',
@ -127,4 +151,6 @@ export class BaseDefDecoratorHandler implements
export interface R3BaseRefDecoratorDetection {
inputs?: Array<{property: ClassMember, decorator: Decorator}>;
outputs?: Array<{property: ClassMember, decorator: Decorator}>;
viewQueries?: {member: ClassMember, decorators: Decorator[]}[];
queries?: {member: ClassMember, decorators: Decorator[]}[];
}

View File

@ -2890,6 +2890,17 @@ describe('compiler compliance', () => {
});
describe('inherited base classes', () => {
const directive = {
'some.directive.ts': `
import {Directive} from '@angular/core';
@Directive({
selector: '[someDir]',
})
export class SomeDirective { }
`
};
it('should add ngBaseDef if one or more @Input is present', () => {
const files = {
app: {
@ -3033,6 +3044,182 @@ describe('compiler compliance', () => {
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should add ngBaseDef if a ViewChild query is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, ViewChild} from '@angular/core';
export class BaseClass {
@ViewChild('something') something: any;
}
@Component({
selector: 'my-component',
template: ''
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const expectedOutput = `
const $e0_attrs$ = ["something"];
// ...
BaseClass.ngBaseDef = i0.ɵɵdefineBase({
viewQuery: function (rf, ctx) {
if (rf & 1) {
$r3$.ɵɵviewQuery($e0_attrs$, true, null);
}
if (rf & 2) {
var $tmp$;
($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.something = $tmp$.first));
}
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should add ngBaseDef if a ViewChildren query is present', () => {
const files = {
app: {
...directive,
'spec.ts': `
import {Component, NgModule, ViewChildren} from '@angular/core';
import {SomeDirective} from './some.directive';
export class BaseClass {
@ViewChildren(SomeDirective) something: QueryList<SomeDirective>;
}
@Component({
selector: 'my-component',
template: ''
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent, SomeDirective]
})
export class MyModule {}
`
}
};
const expectedOutput = `
// ...
BaseClass.ngBaseDef = i0.ɵɵdefineBase({
viewQuery: function (rf, ctx) {
if (rf & 1) {
$r3$.ɵɵviewQuery(SomeDirective, true, null);
}
if (rf & 2) {
var $tmp$;
($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.something = $tmp$));
}
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should add ngBaseDef if a ContentChild query is present', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule, ContentChild} from '@angular/core';
export class BaseClass {
@ContentChild('something') something: any;
}
@Component({
selector: 'my-component',
template: ''
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent]
})
export class MyModule {}
`
}
};
const expectedOutput = `
const $e0_attrs$ = ["something"];
// ...
BaseClass.ngBaseDef = i0.ɵɵdefineBase({
contentQueries: function (rf, ctx, dirIndex) {
if (rf & 1) {
$r3$.ɵɵcontentQuery(dirIndex, $e0_attrs$, true, null);
}
if (rf & 2) {
var $tmp$;
($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.something = $tmp$.first));
}
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should add ngBaseDef if a ContentChildren query is present', () => {
const files = {
app: {
...directive,
'spec.ts': `
import {Component, NgModule, ContentChildren} from '@angular/core';
import {SomeDirective} from './some.directive';
export class BaseClass {
@ContentChildren(SomeDirective) something: QueryList<SomeDirective>;
}
@Component({
selector: 'my-component',
template: ''
})
export class MyComponent extends BaseClass {
}
@NgModule({
declarations: [MyComponent, SomeDirective]
})
export class MyModule {}
`
}
};
const expectedOutput = `
// ...
BaseClass.ngBaseDef = i0.ɵɵdefineBase({
contentQueries: function (rf, ctx, dirIndex) {
if (rf & 1) {
$r3$.ɵɵcontentQuery(dirIndex, SomeDirective, false, null);
}
if (rf & 2) {
var $tmp$;
($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.something = $tmp$));
}
}
});
// ...
`;
const result = compile(files, angularFiles);
expectEmit(result.source, expectedOutput, 'Invalid base definition');
});
it('should NOT add ngBaseDef if @Component is present', () => {
const files = {
app: {