diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 6b648511aafcf..286659d8b17c5 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -284,6 +284,9 @@ export class Identifiers { static InheritDefinitionFeature: o.ExternalReference = {name: 'ɵɵInheritDefinitionFeature', moduleName: CORE}; + static CopyDefinitionFeature: + o.ExternalReference = {name: 'ɵɵCopyDefinitionFeature', moduleName: CORE}; + static ProvidersFeature: o.ExternalReference = {name: 'ɵɵProvidersFeature', moduleName: CORE}; static listener: o.ExternalReference = {name: 'ɵɵlistener', moduleName: CORE}; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index eff2b5a2bcbe6..739d8af84f7f6 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -40,6 +40,7 @@ export { ɵɵsetNgModuleScope, ɵɵtemplateRefExtractor, ɵɵProvidersFeature, + ɵɵCopyDefinitionFeature, ɵɵInheritDefinitionFeature, ɵɵNgOnChangesFeature, LifecycleHooksFeature as ɵLifecycleHooksFeature, diff --git a/packages/core/src/render3/features/copy_definition_feature.ts b/packages/core/src/render3/features/copy_definition_feature.ts new file mode 100644 index 0000000000000..3d4bc0f2569c0 --- /dev/null +++ b/packages/core/src/render3/features/copy_definition_feature.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 {ComponentDef, DirectiveDef} from '../interfaces/definition'; +import {isComponentDef} from '../interfaces/type_checks'; + +import {getSuperType} from './inherit_definition_feature'; + +/** + * Fields which exist on either directive or component definitions, and need to be copied from + * parent to child classes by the `ɵɵCopyDefinitionFeature`. + */ +const COPY_DIRECTIVE_FIELDS: (keyof DirectiveDef)[] = [ + // The child class should use the providers of its parent. + 'providersResolver', + + // Not listed here are any fields which are handled by the `ɵɵInheritDefinitionFeature`, such + // as inputs, outputs, and host binding functions. +]; + +/** + * Fields which exist only on component definitions, and need to be copied from parent to child + * classes by the `ɵɵCopyDefinitionFeature`. + * + * The type here allows any field of `ComponentDef` which is not also a property of `DirectiveDef`, + * since those should go in `COPY_DIRECTIVE_FIELDS` above. + */ +const COPY_COMPONENT_FIELDS: Exclude, keyof DirectiveDef>[] = [ + // The child class should use the template function of its parent, including all template + // semantics. + 'template', + 'decls', + 'consts', + 'vars', + 'onPush', + 'ngContentSelectors', + + // The child class should use the CSS styles of its parent, including all styling semantics. + 'styles', + 'encapsulation', + + // The child class should be checked by the runtime in the same way as its parent. + 'schemas', +]; + +/** + * Copies the fields not handled by the `ɵɵInheritDefinitionFeature` from the supertype of a + * definition. + * + * This exists primarily to support ngcc migration of an existing View Engine pattern, where an + * entire decorator is inherited from a parent to a child class. When ngcc detects this case, it + * generates a skeleton definition on the child class, and applies this feature. + * + * The `ɵɵCopyDefinitionFeature` then copies any needed fields from the parent class' definition, + * including things like the component template function. + * + * @param definition The definition of a child class which inherits from a parent class with its + * own definition. + * + * @codeGenApi + */ +export function ɵɵCopyDefinitionFeature(definition: DirectiveDef| ComponentDef): void { + let superType = getSuperType(definition.type) !; + + let superDef: DirectiveDef|ComponentDef|undefined = undefined; + if (isComponentDef(definition)) { + // Don't use getComponentDef/getDirectiveDef. This logic relies on inheritance. + superDef = superType.ɵcmp !; + } else { + // Don't use getComponentDef/getDirectiveDef. This logic relies on inheritance. + superDef = superType.ɵdir !; + } + + // Needed because `definition` fields are readonly. + const defAny = (definition as any); + + // Copy over any fields that apply to either directives or components. + for (const field of COPY_DIRECTIVE_FIELDS) { + defAny[field] = superDef[field]; + } + + if (isComponentDef(superDef)) { + // Copy over any component-specific fields. + for (const field of COPY_COMPONENT_FIELDS) { + defAny[field] = superDef[field]; + } + } +} diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index 618b1fdb4f204..87c35e370b64c 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -14,7 +14,7 @@ import {isComponentDef} from '../interfaces/type_checks'; import {ɵɵNgOnChangesFeature} from './ng_onchanges_feature'; -function getSuperType(type: Type): Type& +export function getSuperType(type: Type): Type& {ɵcmp?: ComponentDef, ɵdir?: DirectiveDef} { return Object.getPrototypeOf(type.prototype).constructor; } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 18dd27c5d6af7..0f56d2fc30de3 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -7,6 +7,7 @@ */ import {LifecycleHooksFeature, renderComponent, whenRendered} from './component'; import {ɵɵdefineBase, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdefineNgModule, ɵɵdefinePipe, ɵɵsetComponentScope, ɵɵsetNgModuleScope} from './definition'; +import {ɵɵCopyDefinitionFeature} from './features/copy_definition_feature'; import {ɵɵInheritDefinitionFeature} from './features/inherit_definition_feature'; import {ɵɵNgOnChangesFeature} from './features/ng_onchanges_feature'; import {ɵɵProvidersFeature} from './features/providers_feature'; @@ -210,6 +211,7 @@ export { ɵɵDirectiveDefWithMeta, DirectiveType, ɵɵNgOnChangesFeature, + ɵɵCopyDefinitionFeature, ɵɵInheritDefinitionFeature, ɵɵProvidersFeature, PipeDef, diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 8f60f5a4d8ab6..01b0f72c97fb9 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -46,6 +46,7 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵtemplateRefExtractor': r3.ɵɵtemplateRefExtractor, 'ɵɵNgOnChangesFeature': r3.ɵɵNgOnChangesFeature, 'ɵɵProvidersFeature': r3.ɵɵProvidersFeature, + 'ɵɵCopyDefinitionFeature': r3.ɵɵCopyDefinitionFeature, 'ɵɵInheritDefinitionFeature': r3.ɵɵInheritDefinitionFeature, 'ɵɵcontainer': r3.ɵɵcontainer, 'ɵɵnextContext': r3.ɵɵnextContext, diff --git a/packages/core/test/acceptance/copy_definition_feature_spec.ts b/packages/core/test/acceptance/copy_definition_feature_spec.ts new file mode 100644 index 0000000000000..ea4c220c70179 --- /dev/null +++ b/packages/core/test/acceptance/copy_definition_feature_spec.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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, NgModule, ɵɵCopyDefinitionFeature as CopyDefinitionFeature, ɵɵInheritDefinitionFeature as InheritDefinitionFeature, ɵɵdefineComponent as defineComponent} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {onlyInIvy} from '@angular/private/testing'; + +describe('Ivy CopyDefinitionFeature', () => { + onlyInIvy('this feature is not required in View Engine') + .it('should copy the template function of a component definition from parent to child', + () => { + + // It would be nice if the base component could be JIT compiled. However, this creates + // a getter for ɵcmp which precludes adding a static definition of that field for the + // child class. + // TODO(alxhub): see if there's a cleaner way to do this. + class BaseComponent { + name !: string; + static ɵcmp = defineComponent({ + type: BaseComponent, + selectors: [['some-cmp']], + decls: 0, + vars: 0, + inputs: {name: 'name'}, + template: function BaseComponent_Template(rf, ctx) { ctx.rendered = true; }, + encapsulation: 2 + }); + static ɵfac = function BaseComponent_Factory(t: any) { + return new (t || BaseComponent)(); + }; + + rendered = false; + } + + class ChildComponent extends BaseComponent { + static ɵcmp = defineComponent({ + type: ChildComponent, + selectors: [['some-cmp']], + features: [InheritDefinitionFeature, CopyDefinitionFeature], + decls: 0, + vars: 0, + template: function ChildComponent_Template(rf, ctx) {}, + encapsulation: 2 + }); + static ɵfac = function ChildComponent_Factory(t: any) { + return new (t || ChildComponent)(); + }; + } + + @NgModule({ + declarations: [ChildComponent], + exports: [ChildComponent], + }) + class Module { + } + + @Component({ + selector: 'test-cmp', + template: '', + }) + class TestCmp { + } + + TestBed.configureTestingModule({ + declarations: [TestCmp], + imports: [Module], + }); + + const fixture = TestBed.createComponent(TestCmp); + + // The child component should have matched and been instantiated. + const child = fixture.debugElement.children[0].componentInstance as ChildComponent; + expect(child instanceof ChildComponent).toBe(true); + + // And the base class template function should've been called. + expect(child.rendered).toBe(true); + + // The input binding should have worked. + expect(child.name).toBe('Success!'); + }); +}); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 4a0795c2fa0e5..6bc18b230fb23 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -756,6 +756,8 @@ export declare function ɵɵcontainerRefreshStart(index: number): void; export declare function ɵɵcontentQuery(directiveIndex: number, predicate: Type | string[], descend: boolean, read?: any): void; +export declare function ɵɵCopyDefinitionFeature(definition: DirectiveDef | ComponentDef): void; + export declare const ɵɵdefaultStyleSanitizer: StyleSanitizeFn; export declare function ɵɵdefineBase(baseDefinition: {