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:

committed by
Alex Rickabaugh

parent
5b864ede13
commit
effb92dfae
@ -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...',
|
||||
() => {
|
||||
|
Reference in New Issue
Block a user