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:

committed by
Andrew Kushnir

parent
8ca208ff59
commit
c7f1b0a97f
@ -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[]}[];
|
||||
}
|
||||
|
@ -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: {
|
||||
|
Reference in New Issue
Block a user