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

@ -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);
}