Skip to content

Commit

Permalink
feat(ivy): add a runtime feature to copy cmp/dir definitions
Browse files Browse the repository at this point in the history
This commit adds CopyDefinitionFeature, which supports the case where an
entire decorator (@component or @directive) is inherited from parent to
child.

The existing inheritance feature, InheritDefinitionFeature, supports merging
of parent and child definitions when both were originally present. This
merges things like inputs, outputs, host bindings, etc.

CopyDefinitionFeature, on the other hand, compensates for a definition that
was missing entirely on the child class, by copying fields that aren't
ordinarily inherited (like the template function itself).

This feature is intended to only be used as part of ngcc code generation.
  • Loading branch information
alxhub committed Oct 23, 2019
1 parent dc38495 commit 52004c4
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/compiler/src/render3/r3_identifiers.ts
Expand Up @@ -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};
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core_render3_private_export.ts
Expand Up @@ -40,6 +40,7 @@ export {
ɵɵsetNgModuleScope,
ɵɵtemplateRefExtractor,
ɵɵProvidersFeature,
ɵɵCopyDefinitionFeature,
ɵɵInheritDefinitionFeature,
ɵɵNgOnChangesFeature,
LifecycleHooksFeature as ɵLifecycleHooksFeature,
Expand Down
67 changes: 67 additions & 0 deletions packages/core/src/render3/features/copy_definition_feature.ts
@@ -0,0 +1,67 @@
/**
* @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 {Type} from '../../interface/type';
import {ComponentDef, DirectiveDef} from '../interfaces/definition';
import {isComponentDef} from '../interfaces/type_checks';


function getSuperType(type: Type<any>): Type<any>&
{ɵcmp?: ComponentDef<any>, ɵdir?: DirectiveDef<any>} {
return Object.getPrototypeOf(type.prototype).constructor;
}

const COPY_DIRECTIVE_FIELDS: (keyof DirectiveDef<unknown>)[] = ['providersResolver'];
const COPY_COMPONENT_FIELDS: Exclude<keyof ComponentDef<unknown>, keyof DirectiveDef<unknown>>[] = [
'template',
'consts',
'vars',
'onPush',
'styles',
'encapsulation',
'schemas',
];

/**
* Merges the definition from a super class to a sub class.
* @param definition The definition that is a SubClass of another directive of component
*
* @codeGenApi
*/
export function ɵɵCopyDefinitionFeature(definition: DirectiveDef<any>| ComponentDef<any>): void {
let superType = getSuperType(definition.type);

if (superType) {
let superDef: DirectiveDef<any>|ComponentDef<any>|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;
}

if (!superDef) {
// Nothing to do?
return;
}

// Needed because `definition` fields are readonly.
const defAny = (definition as any);

for (const field of COPY_DIRECTIVE_FIELDS) {
defAny[field] = superDef[field];
}

if (isComponentDef(superDef)) {
for (const field of COPY_COMPONENT_FIELDS) {
defAny[field] = superDef[field];
}
}
}
}
2 changes: 2 additions & 0 deletions packages/core/src/render3/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -210,6 +211,7 @@ export {
ɵɵDirectiveDefWithMeta,
DirectiveType,
ɵɵNgOnChangesFeature,
ɵɵCopyDefinitionFeature,
ɵɵInheritDefinitionFeature,
ɵɵProvidersFeature,
PipeDef,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/jit/environment.ts
Expand Up @@ -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,
Expand Down
86 changes: 86 additions & 0 deletions 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: '<some-cmp name="Success!"></some-cmp>',
})
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!');
});
});

0 comments on commit 52004c4

Please sign in to comment.