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: {

View File

@ -37,6 +37,8 @@ export interface CompilerFacade {
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DirectiveMetadataFacade): any;
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadataFacade): any;
compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3BaseMetadataFacade):
any;
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
@ -146,6 +148,13 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
changeDetection?: ChangeDetectionStrategy;
}
export interface R3BaseMetadataFacade {
inputs?: {[key: string]: string | [string, string]};
outputs?: {[key: string]: string};
queries?: R3QueryMetadataFacade[];
viewQueries?: R3QueryMetadataFacade[];
}
export type ViewEncapsulation = number;
export type ChangeDetectionStrategy = number;

View File

@ -7,7 +7,7 @@
*/
import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, R3ComponentMetadataFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface';
import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, R3BaseMetadataFacade, R3ComponentMetadataFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface';
import {ConstantPool} from './constant_pool';
import {HostBinding, HostListener, Input, Output, Type} from './core';
import {compileInjectable} from './injectable_compiler_2';
@ -21,7 +21,7 @@ import {R3InjectorMetadata, R3NgModuleMetadata, compileInjector, compileNgModule
import {compilePipeFromMetadata} from './render3/r3_pipe_compiler';
import {R3Reference} from './render3/util';
import {R3DirectiveMetadata, R3QueryMetadata} from './render3/view/api';
import {ParsedHostBindings, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
import {ParsedHostBindings, compileBaseDefFromMetadata, compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
import {makeBindingParser, parseTemplate} from './render3/view/template';
import {ResourceLoader} from './resource_loader';
import {DomElementSchemaRegistry} from './schema/dom_element_schema_registry';
@ -151,6 +151,20 @@ export class CompilerFacadeImpl implements CompilerFacade {
res.expression, angularCoreEnv, `ng:///${facade.name}.js`, preStatements);
}
compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3BaseMetadataFacade):
any {
const constantPool = new ConstantPool();
const meta = {
...facade,
viewQueries: facade.viewQueries ? facade.viewQueries.map(convertToR3QueryMetadata) :
facade.viewQueries,
queries: facade.queries ? facade.queries.map(convertToR3QueryMetadata) : facade.queries
};
const res = compileBaseDefFromMetadata(meta, constantPool);
return this.jitExpression(
res.expression, angularCoreEnv, sourceMapUrl, constantPool.statements);
}
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan {
return r3JitTypeSourceSpan(kind, typeName, sourceUrl);
}

View File

@ -66,11 +66,13 @@ function baseDirectiveFields(
if (meta.queries.length > 0) {
// e.g. `contentQueries: (rf, ctx, dirIndex) => { ... }
definitionMap.set('contentQueries', createContentQueriesFunction(meta, constantPool));
definitionMap.set(
'contentQueries', createContentQueriesFunction(meta.queries, constantPool, meta.name));
}
if (meta.viewQueries.length) {
definitionMap.set('viewQuery', createViewQueriesFunction(meta, constantPool));
definitionMap.set(
'viewQuery', createViewQueriesFunction(meta.viewQueries, constantPool, meta.name));
}
// Initialize hostVarsCount to number of bound host properties (interpolations illegal),
@ -163,13 +165,15 @@ export function compileDirectiveFromMetadata(
export interface R3BaseRefMetaData {
inputs?: {[key: string]: string | [string, string]};
outputs?: {[key: string]: string};
viewQueries?: R3QueryMetadata[];
queries?: R3QueryMetadata[];
}
/**
* Compile a base definition for the render3 runtime as defined by {@link R3BaseRefMetadata}
* @param meta the metadata used for compilation.
*/
export function compileBaseDefFromMetadata(meta: R3BaseRefMetaData) {
export function compileBaseDefFromMetadata(meta: R3BaseRefMetaData, constantPool: ConstantPool) {
const definitionMap = new DefinitionMap();
if (meta.inputs) {
const inputs = meta.inputs;
@ -188,9 +192,14 @@ export function compileBaseDefFromMetadata(meta: R3BaseRefMetaData) {
});
definitionMap.set('outputs', o.literalMap(outputsMap));
}
if (meta.viewQueries && meta.viewQueries.length > 0) {
definitionMap.set('viewQuery', createViewQueriesFunction(meta.viewQueries, constantPool));
}
if (meta.queries && meta.queries.length > 0) {
definitionMap.set('contentQueries', createContentQueriesFunction(meta.queries, constantPool));
}
const expression = o.importExpr(R3.defineBase).callFn([definitionMap.toLiteralMap()]);
const type = new o.ExpressionType(o.importExpr(R3.BaseDef));
return {expression, type};
@ -475,12 +484,12 @@ function convertAttributesToExpressions(attributes: {[name: string]: o.Expressio
// Define and update any content queries
function createContentQueriesFunction(
meta: R3DirectiveMetadata, constantPool: ConstantPool): o.Expression {
queries: R3QueryMetadata[], constantPool: ConstantPool, name?: string): o.Expression {
const createStatements: o.Statement[] = [];
const updateStatements: o.Statement[] = [];
const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME);
for (const query of meta.queries) {
for (const query of queries) {
// creation, e.g. r3.contentQuery(dirIndex, somePredicate, true, null);
const args = [o.variable('dirIndex'), ...prepareQueryParams(query, constantPool) as any];
@ -498,7 +507,7 @@ function createContentQueriesFunction(
updateStatements.push(refresh.and(updateDirective).toStmt());
}
const contentQueriesFnName = meta.name ? `${meta.name}_ContentQueries` : null;
const contentQueriesFnName = name ? `${name}_ContentQueries` : null;
return o.fn(
[
new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null),
@ -549,12 +558,12 @@ function createTypeForDef(meta: R3DirectiveMetadata, typeBase: o.ExternalReferen
// Define and update any view queries
function createViewQueriesFunction(
meta: R3DirectiveMetadata, constantPool: ConstantPool): o.Expression {
viewQueries: R3QueryMetadata[], constantPool: ConstantPool, name?: string): o.Expression {
const createStatements: o.Statement[] = [];
const updateStatements: o.Statement[] = [];
const tempAllocator = temporaryAllocator(updateStatements, TEMPORARY_NAME);
meta.viewQueries.forEach((query: R3QueryMetadata) => {
viewQueries.forEach((query: R3QueryMetadata) => {
const queryInstruction = query.static ? R3.staticViewQuery : R3.viewQuery;
// creation, e.g. r3.viewQuery(somePredicate, true);
@ -572,7 +581,7 @@ function createViewQueriesFunction(
updateStatements.push(refresh.and(updateDirective).toStmt());
});
const viewQueryFnName = meta.name ? `${meta.name}_Query` : null;
const viewQueryFnName = name ? `${name}_Query` : null;
return o.fn(
[new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)],
[

View File

@ -37,6 +37,8 @@ export interface CompilerFacade {
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DirectiveMetadataFacade): any;
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadataFacade): any;
compileBase(angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3BaseMetadataFacade):
any;
createParseSourceSpan(kind: string, typeName: string, sourceUrl: string): ParseSourceSpan;
@ -146,6 +148,13 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
changeDetection?: ChangeDetectionStrategy;
}
export interface R3BaseMetadataFacade {
inputs?: {[key: string]: string | [string, string]};
outputs?: {[key: string]: string};
queries?: R3QueryMetadataFacade[];
viewQueries?: R3QueryMetadataFacade[];
}
export type ViewEncapsulation = number;
export type ChangeDetectionStrategy = number;

View File

@ -9,12 +9,10 @@
import {ChangeDetectionStrategy} from '../change_detection/constants';
import {Provider} from '../di';
import {Type} from '../interface/type';
import {NG_BASE_DEF} from '../render3/fields';
import {compileComponent as render3CompileComponent, compileDirective as render3CompileDirective} from '../render3/jit/directive';
import {compilePipe as render3CompilePipe} from '../render3/jit/pipe';
import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators';
import {noop} from '../util/noop';
import {fillProperties} from '../util/property';
import {ViewEncapsulation} from './view';
@ -695,47 +693,12 @@ export interface Input {
bindingPropertyName?: string;
}
const initializeBaseDef = (target: any): void => {
const constructor = target.constructor;
const inheritedBaseDef = constructor.ngBaseDef;
const baseDef = constructor.ngBaseDef = {
inputs: {},
outputs: {},
declaredInputs: {},
};
if (inheritedBaseDef) {
fillProperties(baseDef.inputs, inheritedBaseDef.inputs);
fillProperties(baseDef.outputs, inheritedBaseDef.outputs);
fillProperties(baseDef.declaredInputs, inheritedBaseDef.declaredInputs);
}
};
/**
* Does the work of creating the `ngBaseDef` property for the `Input` and `Output` decorators.
* @param key "inputs" or "outputs"
*/
const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any}) => any) =>
(target: any, name: string, ...args: any[]) => {
const constructor = target.constructor;
if (!constructor.hasOwnProperty(NG_BASE_DEF)) {
initializeBaseDef(target);
}
const baseDef = constructor.ngBaseDef;
const defProp = getProp(baseDef);
defProp[name] = args[0] || name;
};
/**
* @Annotation
* @publicApi
*/
export const Input: InputDecorator = makePropDecorator(
'Input', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
updateBaseDefFromIOProp(baseDef => baseDef.inputs || {}));
export const Input: InputDecorator =
makePropDecorator('Input', (bindingPropertyName?: string) => ({bindingPropertyName}));
/**
* Type of the Output decorator / constructor function.
@ -777,9 +740,8 @@ export interface Output {
* @Annotation
* @publicApi
*/
export const Output: OutputDecorator = makePropDecorator(
'Output', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
updateBaseDefFromIOProp(baseDef => baseDef.outputs || {}));
export const Output: OutputDecorator =
makePropDecorator('Output', (bindingPropertyName?: string) => ({bindingPropertyName}));

View File

@ -18,7 +18,7 @@ import {noSideEffects} from '../util/closure';
import {stringify} from '../util/stringify';
import {EMPTY_ARRAY, EMPTY_OBJ} from './empty';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields';
import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields';
import {ComponentDef, ComponentDefFeature, ComponentTemplate, ComponentType, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, DirectiveType, DirectiveTypesOrFactory, FactoryFn, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory, ViewQueriesFunction, ɵɵBaseDef} from './interfaces/definition';
// while SelectorFlags is unused here, it's required so that types don't get resolved lazily
// see: https://github.com/Microsoft/web-build-tools/issues/1050
@ -560,12 +560,25 @@ export function ɵɵdefineBase<T>(baseDefinition: {
* of properties.
*/
outputs?: {[P in keyof T]?: string};
/**
* Function to create instances of content queries associated with a given directive.
*/
contentQueries?: ContentQueriesFunction<T>| null;
/**
* Additional set of instructions specific to view query processing. This could be seen as a
* set of instructions to be inserted into the template function.
*/
viewQuery?: ViewQueriesFunction<T>| null;
}): ɵɵBaseDef<T> {
const declaredInputs: {[P in keyof T]: string} = {} as any;
return {
inputs: invertObject<T>(baseDefinition.inputs as any, declaredInputs),
declaredInputs: declaredInputs,
outputs: invertObject<T>(baseDefinition.outputs as any),
viewQuery: baseDefinition.viewQuery || null,
contentQueries: baseDefinition.contentQueries || null,
};
}
@ -742,6 +755,10 @@ export function getPipeDef<T>(type: any): PipeDef<T>|null {
return (type as any)[NG_PIPE_DEF] || null;
}
export function getBaseDef<T>(type: any): ɵɵBaseDef<T>|null {
return (type as any)[NG_BASE_DEF] || null;
}
export function getNgModuleDef<T>(type: any, throwNotFound: true): NgModuleDef<T>;
export function getNgModuleDef<T>(type: any): NgModuleDef<T>|null;
export function getNgModuleDef<T>(type: any, throwNotFound?: boolean): NgModuleDef<T>|null {

View File

@ -9,7 +9,7 @@
import {Type} from '../../interface/type';
import {fillProperties} from '../../util/property';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
import {ComponentDef, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {adjustActiveDirectiveSuperClassDepthPosition} from '../state';
import {isComponentDef} from '../util/view_utils';
@ -54,7 +54,10 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>| Comp
}
if (baseDef) {
// Merge inputs and outputs
const baseViewQuery = baseDef.viewQuery;
const baseContentQueries = baseDef.contentQueries;
baseViewQuery && inheritViewQuery(definition, baseViewQuery);
baseContentQueries && inheritContentQueries(definition, baseContentQueries);
fillProperties(definition.inputs, baseDef.inputs);
fillProperties(definition.declaredInputs, baseDef.declaredInputs);
fillProperties(definition.outputs, baseDef.outputs);
@ -91,34 +94,11 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>| Comp
}
}
// Merge View Queries
const prevViewQuery = definition.viewQuery;
// Merge queries
const superViewQuery = superDef.viewQuery;
if (superViewQuery) {
if (prevViewQuery) {
definition.viewQuery = <T>(rf: RenderFlags, ctx: T): void => {
superViewQuery(rf, ctx);
prevViewQuery(rf, ctx);
};
} else {
definition.viewQuery = superViewQuery;
}
}
// Merge Content Queries
const prevContentQueries = definition.contentQueries;
const superContentQueries = superDef.contentQueries;
if (superContentQueries) {
if (prevContentQueries) {
definition.contentQueries = <T>(rf: RenderFlags, ctx: T, directiveIndex: number) => {
superContentQueries(rf, ctx, directiveIndex);
prevContentQueries(rf, ctx, directiveIndex);
};
} else {
definition.contentQueries = superContentQueries;
}
}
superViewQuery && inheritViewQuery(definition, superViewQuery);
superContentQueries && inheritContentQueries(definition, superContentQueries);
// Merge inputs and outputs
fillProperties(definition.inputs, superDef.inputs);
@ -181,3 +161,32 @@ function maybeUnwrapEmpty(value: any): any {
return value;
}
}
function inheritViewQuery(
definition: DirectiveDef<any>| ComponentDef<any>, superViewQuery: ViewQueriesFunction<any>) {
const prevViewQuery = definition.viewQuery;
if (prevViewQuery) {
definition.viewQuery = (rf, ctx) => {
superViewQuery(rf, ctx);
prevViewQuery(rf, ctx);
};
} else {
definition.viewQuery = superViewQuery;
}
}
function inheritContentQueries(
definition: DirectiveDef<any>| ComponentDef<any>,
superContentQueries: ContentQueriesFunction<any>) {
const prevContentQueries = definition.contentQueries;
if (prevContentQueries) {
definition.contentQueries = (rf, ctx, directiveIndex) => {
superContentQueries(rf, ctx, directiveIndex);
prevContentQueries(rf, ctx, directiveIndex);
};
} else {
definition.contentQueries = superContentQueries;
}
}

View File

@ -96,7 +96,7 @@ export type ɵɵDirectiveDefWithMeta<
* Runtime information for classes that are inherited by components or directives
* that aren't defined as components or directives.
*
* This is an internal data structure used by the render to determine what inputs
* This is an internal data structure used by the renderer to determine what inputs
* and outputs should be inherited.
*
* See: {@link defineBase}
@ -123,6 +123,18 @@ export interface ɵɵBaseDef<T> {
* (as in `@Output('alias') propertyName: any;`).
*/
readonly outputs: {[P in keyof T]: string};
/**
* Function to create and refresh content queries associated with a given directive.
*/
contentQueries: ContentQueriesFunction<T>|null;
/**
* Query-related instructions for a directive. Note that while directives don't have a
* view and as such view queries won't necessarily do anything, there might be
* components that extend the directive.
*/
viewQuery: ViewQueriesFunction<T>|null;
}
/**
@ -161,18 +173,6 @@ export interface DirectiveDef<T> extends ɵɵBaseDef<T> {
*/
factory: FactoryFn<T>;
/**
* Function to create and refresh content queries associated with a given directive.
*/
contentQueries: ContentQueriesFunction<T>|null;
/**
* Query-related instructions for a directive. Note that while directives don't have a
* view and as such view queries won't necessarily do anything, there might be
* components that extend the directive.
*/
viewQuery: ViewQueriesFunction<T>|null;
/**
* Refreshes host bindings on the associated directive.
*/

View File

@ -7,7 +7,7 @@
*/
import {R3DirectiveMetadataFacade, getCompilerFacade} from '../../compiler/compiler_facade';
import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface';
import {R3BaseMetadataFacade, R3ComponentMetadataFacade, R3QueryMetadataFacade} from '../../compiler/compiler_facade_interface';
import {resolveForwardRef} from '../../di/forward_ref';
import {compileInjectable} from '../../di/jit/injectable';
import {getReflect, reflectDependencies} from '../../di/jit/util';
@ -16,8 +16,9 @@ import {Query} from '../../metadata/di';
import {Component, Directive, Input} from '../../metadata/directives';
import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
import {ViewEncapsulation} from '../../metadata/view';
import {getBaseDef, getComponentDef, getDirectiveDef} from '../definition';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields';
import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields';
import {ComponentType} from '../interfaces/definition';
import {renderStringify} from '../util/misc_utils';
@ -71,6 +72,9 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
interpolation: metadata.interpolation,
viewProviders: metadata.viewProviders || null,
};
if (meta.usesInheritance) {
addBaseDefToUndecoratedParents(type);
}
ngComponentDef = compiler.compileComponent(angularCoreEnv, templateUrl, meta);
// When NgModule decorator executed, we enqueued the module definition such that
@ -125,6 +129,9 @@ export function compileDirective(type: Type<any>, directive: Directive): void {
const facade = directiveMetadata(type as ComponentType<any>, directive);
facade.typeSourceSpan =
compiler.createParseSourceSpan('Directive', renderStringify(type), sourceMapUrl);
if (facade.usesInheritance) {
addBaseDefToUndecoratedParents(type);
}
ngDirectiveDef = compiler.compileDirective(angularCoreEnv, sourceMapUrl, facade);
}
return ngDirectiveDef;
@ -171,6 +178,71 @@ export function directiveMetadata(type: Type<any>, metadata: Directive): R3Direc
};
}
/**
* Adds an `ngBaseDef` to all parent classes of a type that don't have an Angular decorator.
*/
function addBaseDefToUndecoratedParents(type: Type<any>) {
const objPrototype = Object.prototype;
let parent = Object.getPrototypeOf(type);
// Go up the prototype until we hit `Object`.
while (parent && parent !== objPrototype) {
// Since inheritance works if the class was annotated already, we only need to add
// the base def if there are no annotations and the base def hasn't been created already.
if (!getDirectiveDef(parent) && !getComponentDef(parent) && !getBaseDef(parent)) {
const facade = extractBaseDefMetadata(parent);
facade && compileBase(parent, facade);
}
parent = Object.getPrototypeOf(parent);
}
}
/** Compiles the base metadata into a base definition. */
function compileBase(type: Type<any>, facade: R3BaseMetadataFacade): void {
let ngBaseDef: any = null;
Object.defineProperty(type, NG_BASE_DEF, {
get: () => {
if (ngBaseDef === null) {
const name = type && type.name;
const sourceMapUrl = `ng://${name}/ngBaseDef.js`;
const compiler = getCompilerFacade();
ngBaseDef = compiler.compileBase(angularCoreEnv, sourceMapUrl, facade);
}
return ngBaseDef;
},
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode,
});
}
/** Extracts the metadata necessary to construct an `ngBaseDef` from a class. */
function extractBaseDefMetadata(type: Type<any>): R3BaseMetadataFacade|null {
const propMetadata = getReflect().ownPropMetadata(type);
const viewQueries = extractQueriesMetadata(type, propMetadata, isViewQuery);
const queries = extractQueriesMetadata(type, propMetadata, isContentQuery);
let inputs: {[key: string]: string | [string, string]}|undefined;
let outputs: {[key: string]: string}|undefined;
for (const field in propMetadata) {
propMetadata[field].forEach(ann => {
if (ann.ngMetadataName === 'Input') {
inputs = inputs || {};
inputs[field] = ann.bindingPropertyName ? [ann.bindingPropertyName, field] : field;
} else if (ann.ngMetadataName === 'Output') {
outputs = outputs || {};
outputs[field] = ann.bindingPropertyName || field;
}
});
}
// Only generate the base def if there's any info inside it.
if (inputs || outputs || viewQueries.length || queries.length) {
return {inputs, outputs, viewQueries, queries};
}
return null;
}
function convertToR3QueryPredicate(selector: any): any|string[] {
return typeof selector === 'string' ? splitByComma(selector) : resolveForwardRef(selector);
}

View File

@ -5,11 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ContentChild, Directive, HostBinding, HostListener, Input, QueryList, TemplateRef, ViewChildren} from '@angular/core';
import {Component, ContentChild, Directive, EventEmitter, HostListener, Input, Output, QueryList, TemplateRef, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
describe('acceptance integration tests', () => {
it('should only call inherited host listeners once', () => {
@ -117,4 +116,59 @@ describe('acceptance integration tests', () => {
expect(fixture.componentInstance.tpl).not.toBeNull();
expect(fixture.debugElement.nativeElement.getAttribute('aria-disabled')).toBe('true');
});
it('should inherit inputs from undecorated superclasses', () => {
class ButtonSuperClass {
@Input() isDisabled !: boolean;
}
@Component({selector: 'button[custom-button]', template: ''})
class ButtonSubClass extends ButtonSuperClass {
}
@Component({template: '<button custom-button [isDisabled]="disableButton"></button>'})
class MyApp {
disableButton = false;
}
TestBed.configureTestingModule({declarations: [MyApp, ButtonSubClass]});
const fixture = TestBed.createComponent(MyApp);
const button = fixture.debugElement.query(By.directive(ButtonSubClass)).componentInstance;
fixture.detectChanges();
expect(button.isDisabled).toBe(false);
fixture.componentInstance.disableButton = true;
fixture.detectChanges();
expect(button.isDisabled).toBe(true);
});
it('should inherit outputs from undecorated superclasses', () => {
let clicks = 0;
class ButtonSuperClass {
@Output() clicked = new EventEmitter<void>();
emitClick() { this.clicked.emit(); }
}
@Component({selector: 'button[custom-button]', template: ''})
class ButtonSubClass extends ButtonSuperClass {
}
@Component({template: '<button custom-button (clicked)="handleClick()"></button>'})
class MyApp {
handleClick() { clicks++; }
}
TestBed.configureTestingModule({declarations: [MyApp, ButtonSubClass]});
const fixture = TestBed.createComponent(MyApp);
const button = fixture.debugElement.query(By.directive(ButtonSubClass)).componentInstance;
button.emitClick();
fixture.detectChanges();
expect(clicks).toBe(1);
});
});

View File

@ -173,6 +173,96 @@ describe('query logic', () => {
.toBe(true);
});
it('should support ViewChild query inherited from undecorated superclasses', () => {
class MyComp {
@ViewChild('foo') foo: any;
}
@Component({selector: 'sub-comp', template: '<div #foo></div>'})
class SubComp extends MyComp {
}
TestBed.configureTestingModule({declarations: [SubComp]});
const fixture = TestBed.createComponent(SubComp);
fixture.detectChanges();
expect(fixture.componentInstance.foo).toBeAnInstanceOf(ElementRef);
});
it('should support ViewChild query inherited from undecorated grand superclasses', () => {
class MySuperComp {
@ViewChild('foo') foo: any;
}
class MyComp extends MySuperComp {}
@Component({selector: 'sub-comp', template: '<div #foo></div>'})
class SubComp extends MyComp {
}
TestBed.configureTestingModule({declarations: [SubComp]});
const fixture = TestBed.createComponent(SubComp);
fixture.detectChanges();
expect(fixture.componentInstance.foo).toBeAnInstanceOf(ElementRef);
});
it('should support ViewChildren query inherited from undecorated superclasses', () => {
@Directive({selector: '[some-dir]'})
class SomeDir {
}
class MyComp {
@ViewChildren(SomeDir) foo !: QueryList<SomeDir>;
}
@Component({
selector: 'sub-comp',
template: `
<div some-dir></div>
<div some-dir></div>
`
})
class SubComp extends MyComp {
}
TestBed.configureTestingModule({declarations: [SubComp, SomeDir]});
const fixture = TestBed.createComponent(SubComp);
fixture.detectChanges();
expect(fixture.componentInstance.foo).toBeAnInstanceOf(QueryList);
expect(fixture.componentInstance.foo.length).toBe(2);
});
it('should support ViewChildren query inherited from undecorated grand superclasses', () => {
@Directive({selector: '[some-dir]'})
class SomeDir {
}
class MySuperComp {
@ViewChildren(SomeDir) foo !: QueryList<SomeDir>;
}
class MyComp extends MySuperComp {}
@Component({
selector: 'sub-comp',
template: `
<div some-dir></div>
<div some-dir></div>
`
})
class SubComp extends MyComp {
}
TestBed.configureTestingModule({declarations: [SubComp, SomeDir]});
const fixture = TestBed.createComponent(SubComp);
fixture.detectChanges();
expect(fixture.componentInstance.foo).toBeAnInstanceOf(QueryList);
expect(fixture.componentInstance.foo.length).toBe(2);
});
});
describe('content queries', () => {
@ -399,6 +489,118 @@ describe('query logic', () => {
expect(secondComponent.setEvents).toEqual(['textDir set', 'foo set']);
});
it('should support ContentChild query inherited from undecorated superclasses', () => {
class MyComp {
@ContentChild('foo') foo: any;
}
@Component({selector: 'sub-comp', template: '<ng-content></ng-content>'})
class SubComp extends MyComp {
}
@Component({template: '<sub-comp><div #foo></div></sub-comp>'})
class App {
@ViewChild(SubComp) subComp !: SubComp;
}
TestBed.configureTestingModule({declarations: [App, SubComp]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(ElementRef);
});
it('should support ContentChild query inherited from undecorated grand superclasses', () => {
class MySuperComp {
@ContentChild('foo') foo: any;
}
class MyComp extends MySuperComp {}
@Component({selector: 'sub-comp', template: '<ng-content></ng-content>'})
class SubComp extends MyComp {
}
@Component({template: '<sub-comp><div #foo></div></sub-comp>'})
class App {
@ViewChild(SubComp) subComp !: SubComp;
}
TestBed.configureTestingModule({declarations: [App, SubComp]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(ElementRef);
});
it('should support ContentChildren query inherited from undecorated superclasses', () => {
@Directive({selector: '[some-dir]'})
class SomeDir {
}
class MyComp {
@ContentChildren(SomeDir) foo !: QueryList<SomeDir>;
}
@Component({selector: 'sub-comp', template: '<ng-content></ng-content>'})
class SubComp extends MyComp {
}
@Component({
template: `
<sub-comp>
<div some-dir></div>
<div some-dir></div>
</sub-comp>
`
})
class App {
@ViewChild(SubComp) subComp !: SubComp;
}
TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(QueryList);
expect(fixture.componentInstance.subComp.foo.length).toBe(2);
});
it('should support ContentChildren query inherited from undecorated grand superclasses', () => {
@Directive({selector: '[some-dir]'})
class SomeDir {
}
class MySuperComp {
@ContentChildren(SomeDir) foo !: QueryList<SomeDir>;
}
class MyComp extends MySuperComp {}
@Component({selector: 'sub-comp', template: '<ng-content></ng-content>'})
class SubComp extends MyComp {
}
@Component({
template: `
<sub-comp>
<div some-dir></div>
<div some-dir></div>
</sub-comp>
`
})
class App {
@ViewChild(SubComp) subComp !: SubComp;
}
TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(QueryList);
expect(fixture.componentInstance.subComp.foo.length).toBe(2);
});
});
// Some root components may have ContentChildren queries if they are also

View File

@ -283,32 +283,6 @@ ivyEnabled && describe('render3 jit', () => {
expect(InputDirAny.ngDirectiveDef.declaredInputs).toEqual({publicName: 'privateName'});
});
it('should add ngBaseDef to types with @Input properties', () => {
class C {
@Input('alias1')
prop1 = 'test';
@Input('alias2')
prop2 = 'test';
}
expect((C as any).ngBaseDef).toBeDefined();
expect((C as any).ngBaseDef.inputs).toEqual({prop1: 'alias1', prop2: 'alias2'});
});
it('should add ngBaseDef to types with @Output properties', () => {
class C {
@Output('alias1')
prop1 = 'test';
@Output('alias2')
prop2 = 'test';
}
expect((C as any).ngBaseDef).toBeDefined();
expect((C as any).ngBaseDef.outputs).toEqual({prop1: 'alias1', prop2: 'alias2'});
});
it('should compile ContentChildren query with string predicate on a directive', () => {
@Directive({selector: '[test]'})
class TestDirective {

View File

@ -662,6 +662,7 @@ export interface OutputDecorator {
export declare function ɵɵallocHostVars(count: number): void;
export interface ɵɵBaseDef<T> {
contentQueries: ContentQueriesFunction<T> | null;
/** @deprecated */ readonly declaredInputs: {
[P in keyof T]: string;
};
@ -671,6 +672,7 @@ export interface ɵɵBaseDef<T> {
readonly outputs: {
[P in keyof T]: string;
};
viewQuery: ViewQueriesFunction<T> | null;
}
export declare function ɵɵbind<T>(value: T): T | NO_CHANGE;
@ -702,6 +704,8 @@ export declare function ɵɵdefineBase<T>(baseDefinition: {
outputs?: {
[P in keyof T]?: string;
};
contentQueries?: ContentQueriesFunction<T> | null;
viewQuery?: ViewQueriesFunction<T> | null;
}): ɵɵBaseDef<T>;
export declare function ɵɵdefineComponent<T>(componentDefinition: {