fix(ivy): skip field inheritance if InheritDefinitionFeature is present on parent def (#34244)

The main logic of the `InheritDefinitionFeature` is to go through the prototype chain of a given Component and merge all Angular-specific information onto that Component def. The problem happens in case there is a Component in a hierarchy that also contains the `InheritDefinitionFeature` (i.e. it extends some other Component), so it inherits all Angular-specific information from its super class. As a result, the root Component may end up having duplicate information inherited from different Components in hierarchy.

Let's consider the following structure: `GrandChild` extends `Child` that extends `Base` and the `Base` class has a `HostListener`. In this scenario `GrandChild` and `Child` will have `InheritDefinitionFeature` included into the `features` list. The processing will happend in the following order:

- `Child` inherits `HostListener` from the `Base` class
- `GrandChild` inherits `HostListener` from the `Child` class
- since `Child` has a parent, `GrandChild` also inherits from the `Base` class

The result is that the `GrandChild` def has duplicated host listener, which is not correct.

This commit introduces additional logic that checks whether we came across a def that has `InheritDefinitionFeature` feature (which means that this def already inherited information from its super classes). If that's the case, we skip further fields-related inheritance logic, but keep going though the prototype chain to look for super classes that contain other features (like NgOnChanges), that we need to invoke for a given Component def.

PR Close #34244
This commit is contained in:
Andrew Kushnir
2019-12-04 23:21:59 -08:00
committed by Alex Rickabaugh
parent 5b864ede13
commit effb92dfae
2 changed files with 114 additions and 43 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ContentChildren, Directive, EventEmitter, HostBinding, Input, OnChanges, Output, QueryList, ViewChildren} from '@angular/core';
import {Component, ContentChildren, Directive, EventEmitter, HostBinding, HostListener, Input, OnChanges, Output, QueryList, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {onlyInIvy} from '@angular/private/testing';
@ -4225,6 +4225,71 @@ describe('inheritance', () => {
expect(foundQueryList !.length).toBe(5);
});
it('should inherit host listeners from base class once', () => {
const events: string[] = [];
@Component({
selector: 'app-base',
template: 'base',
})
class BaseComponent {
@HostListener('click')
clicked() { events.push('BaseComponent.clicked'); }
}
@Component({
selector: 'app-child',
template: 'child',
})
class ChildComponent extends BaseComponent {
// additional host listeners are defined here to have `hostBindings` function generated on
// component def, which would trigger `hostBindings` functions merge operation in
// InheritDefinitionFeature logic (merging Child and Base host binding functions)
@HostListener('focus')
focused() {}
clicked() { events.push('ChildComponent.clicked'); }
}
@Component({
selector: 'app-grand-child',
template: 'grand-child',
})
class GrandChildComponent extends ChildComponent {
// additional host listeners are defined here to have `hostBindings` function generated on
// component def, which would trigger `hostBindings` functions merge operation in
// InheritDefinitionFeature logic (merging GrandChild and Child host binding functions)
@HostListener('blur')
blurred() {}
clicked() { events.push('GrandChildComponent.clicked'); }
}
@Component({
selector: 'root-app',
template: `
<app-base></app-base>
<app-child></app-child>
<app-grand-child></app-grand-child>
`,
})
class RootApp {
}
const components = [BaseComponent, ChildComponent, GrandChildComponent];
TestBed.configureTestingModule({
declarations: [RootApp, ...components],
});
const fixture = TestBed.createComponent(RootApp);
fixture.detectChanges();
components.forEach(component => {
fixture.debugElement.query(By.directive(component)).nativeElement.click();
});
expect(events).toEqual(
['BaseComponent.clicked', 'ChildComponent.clicked', 'GrandChildComponent.clicked']);
});
xdescribe(
'what happens when...',
() => {