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
@ -5,11 +5,10 @@
|
||||
* 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, ContentChild, Directive, HostBinding, HostListener, Input, QueryList, TemplateRef, ViewChildren} from '@angular/core';
|
||||
import {Component, ContentChild, Directive, EventEmitter, HostListener, Input, Output, QueryList, TemplateRef, ViewChildren} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
describe('acceptance integration tests', () => {
|
||||
it('should only call inherited host listeners once', () => {
|
||||
@ -117,4 +116,59 @@ describe('acceptance integration tests', () => {
|
||||
expect(fixture.componentInstance.tpl).not.toBeNull();
|
||||
expect(fixture.debugElement.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it('should inherit inputs from undecorated superclasses', () => {
|
||||
class ButtonSuperClass {
|
||||
@Input() isDisabled !: boolean;
|
||||
}
|
||||
|
||||
@Component({selector: 'button[custom-button]', template: ''})
|
||||
class ButtonSubClass extends ButtonSuperClass {
|
||||
}
|
||||
|
||||
@Component({template: '<button custom-button [isDisabled]="disableButton"></button>'})
|
||||
class MyApp {
|
||||
disableButton = false;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyApp, ButtonSubClass]});
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
const button = fixture.debugElement.query(By.directive(ButtonSubClass)).componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(button.isDisabled).toBe(false);
|
||||
|
||||
fixture.componentInstance.disableButton = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(button.isDisabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should inherit outputs from undecorated superclasses', () => {
|
||||
let clicks = 0;
|
||||
|
||||
class ButtonSuperClass {
|
||||
@Output() clicked = new EventEmitter<void>();
|
||||
emitClick() { this.clicked.emit(); }
|
||||
}
|
||||
|
||||
@Component({selector: 'button[custom-button]', template: ''})
|
||||
class ButtonSubClass extends ButtonSuperClass {
|
||||
}
|
||||
|
||||
@Component({template: '<button custom-button (clicked)="handleClick()"></button>'})
|
||||
class MyApp {
|
||||
handleClick() { clicks++; }
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyApp, ButtonSubClass]});
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
const button = fixture.debugElement.query(By.directive(ButtonSubClass)).componentInstance;
|
||||
|
||||
button.emitClick();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(clicks).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -173,6 +173,96 @@ describe('query logic', () => {
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should support ViewChild query inherited from undecorated superclasses', () => {
|
||||
class MyComp {
|
||||
@ViewChild('foo') foo: any;
|
||||
}
|
||||
|
||||
@Component({selector: 'sub-comp', template: '<div #foo></div>'})
|
||||
class SubComp extends MyComp {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [SubComp]});
|
||||
|
||||
const fixture = TestBed.createComponent(SubComp);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.foo).toBeAnInstanceOf(ElementRef);
|
||||
});
|
||||
|
||||
it('should support ViewChild query inherited from undecorated grand superclasses', () => {
|
||||
class MySuperComp {
|
||||
@ViewChild('foo') foo: any;
|
||||
}
|
||||
|
||||
class MyComp extends MySuperComp {}
|
||||
|
||||
@Component({selector: 'sub-comp', template: '<div #foo></div>'})
|
||||
class SubComp extends MyComp {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [SubComp]});
|
||||
|
||||
const fixture = TestBed.createComponent(SubComp);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.foo).toBeAnInstanceOf(ElementRef);
|
||||
});
|
||||
|
||||
it('should support ViewChildren query inherited from undecorated superclasses', () => {
|
||||
@Directive({selector: '[some-dir]'})
|
||||
class SomeDir {
|
||||
}
|
||||
|
||||
class MyComp {
|
||||
@ViewChildren(SomeDir) foo !: QueryList<SomeDir>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'sub-comp',
|
||||
template: `
|
||||
<div some-dir></div>
|
||||
<div some-dir></div>
|
||||
`
|
||||
})
|
||||
class SubComp extends MyComp {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [SubComp, SomeDir]});
|
||||
|
||||
const fixture = TestBed.createComponent(SubComp);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.foo).toBeAnInstanceOf(QueryList);
|
||||
expect(fixture.componentInstance.foo.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should support ViewChildren query inherited from undecorated grand superclasses', () => {
|
||||
@Directive({selector: '[some-dir]'})
|
||||
class SomeDir {
|
||||
}
|
||||
|
||||
class MySuperComp {
|
||||
@ViewChildren(SomeDir) foo !: QueryList<SomeDir>;
|
||||
}
|
||||
|
||||
class MyComp extends MySuperComp {}
|
||||
|
||||
@Component({
|
||||
selector: 'sub-comp',
|
||||
template: `
|
||||
<div some-dir></div>
|
||||
<div some-dir></div>
|
||||
`
|
||||
})
|
||||
class SubComp extends MyComp {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [SubComp, SomeDir]});
|
||||
|
||||
const fixture = TestBed.createComponent(SubComp);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.foo).toBeAnInstanceOf(QueryList);
|
||||
expect(fixture.componentInstance.foo.length).toBe(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('content queries', () => {
|
||||
@ -399,6 +489,118 @@ describe('query logic', () => {
|
||||
expect(secondComponent.setEvents).toEqual(['textDir set', 'foo set']);
|
||||
});
|
||||
|
||||
it('should support ContentChild query inherited from undecorated superclasses', () => {
|
||||
class MyComp {
|
||||
@ContentChild('foo') foo: any;
|
||||
}
|
||||
|
||||
@Component({selector: 'sub-comp', template: '<ng-content></ng-content>'})
|
||||
class SubComp extends MyComp {
|
||||
}
|
||||
|
||||
@Component({template: '<sub-comp><div #foo></div></sub-comp>'})
|
||||
class App {
|
||||
@ViewChild(SubComp) subComp !: SubComp;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [App, SubComp]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(ElementRef);
|
||||
});
|
||||
|
||||
it('should support ContentChild query inherited from undecorated grand superclasses', () => {
|
||||
class MySuperComp {
|
||||
@ContentChild('foo') foo: any;
|
||||
}
|
||||
|
||||
class MyComp extends MySuperComp {}
|
||||
|
||||
@Component({selector: 'sub-comp', template: '<ng-content></ng-content>'})
|
||||
class SubComp extends MyComp {
|
||||
}
|
||||
|
||||
@Component({template: '<sub-comp><div #foo></div></sub-comp>'})
|
||||
class App {
|
||||
@ViewChild(SubComp) subComp !: SubComp;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [App, SubComp]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(ElementRef);
|
||||
});
|
||||
|
||||
it('should support ContentChildren query inherited from undecorated superclasses', () => {
|
||||
@Directive({selector: '[some-dir]'})
|
||||
class SomeDir {
|
||||
}
|
||||
|
||||
class MyComp {
|
||||
@ContentChildren(SomeDir) foo !: QueryList<SomeDir>;
|
||||
}
|
||||
|
||||
@Component({selector: 'sub-comp', template: '<ng-content></ng-content>'})
|
||||
class SubComp extends MyComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<sub-comp>
|
||||
<div some-dir></div>
|
||||
<div some-dir></div>
|
||||
</sub-comp>
|
||||
`
|
||||
})
|
||||
class App {
|
||||
@ViewChild(SubComp) subComp !: SubComp;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(QueryList);
|
||||
expect(fixture.componentInstance.subComp.foo.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should support ContentChildren query inherited from undecorated grand superclasses', () => {
|
||||
@Directive({selector: '[some-dir]'})
|
||||
class SomeDir {
|
||||
}
|
||||
|
||||
class MySuperComp {
|
||||
@ContentChildren(SomeDir) foo !: QueryList<SomeDir>;
|
||||
}
|
||||
|
||||
class MyComp extends MySuperComp {}
|
||||
|
||||
@Component({selector: 'sub-comp', template: '<ng-content></ng-content>'})
|
||||
class SubComp extends MyComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<sub-comp>
|
||||
<div some-dir></div>
|
||||
<div some-dir></div>
|
||||
</sub-comp>
|
||||
`
|
||||
})
|
||||
class App {
|
||||
@ViewChild(SubComp) subComp !: SubComp;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.subComp.foo).toBeAnInstanceOf(QueryList);
|
||||
expect(fixture.componentInstance.subComp.foo.length).toBe(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Some root components may have ContentChildren queries if they are also
|
||||
|
Reference in New Issue
Block a user