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:
Kristiyan Kostadinov
2019-04-21 17:37:15 +02:00
committed by Andrew Kushnir
parent 8ca208ff59
commit c7f1b0a97f
15 changed files with 688 additions and 140 deletions

View File

@ -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);
});
});

View File

@ -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