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
@ -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;
|
||||
|
@ -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}));
|
||||
|
||||
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user