fix(core): handle synthetic props in Directive host bindings correctly (#35568)

Prior to this change, animations-related runtime logic assumed that the @HostBinding and @HostListener with synthetic (animations) props are used for Components only. However having @HostBinding and @HostListener with synthetic props on Directives is also supported by View Engine. This commit updates the logic to select correct renderer to execute instructions (current renderer for Directives and sub-component renderer for Components).

This PR resolves #35501.

PR Close #35568
This commit is contained in:
Andrew Kushnir
2020-02-18 17:49:37 -08:00
parent 2a27f69522
commit 0f389fa5ae
9 changed files with 377 additions and 31 deletions

View File

@ -6,10 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {state, style, transition, trigger} from '@angular/animations';
import {CommonModule} from '@angular/common';
import {AfterContentInit, Component, ComponentFactoryResolver, ComponentRef, ContentChildren, Directive, DoCheck, HostBinding, HostListener, Injectable, Input, NgModule, OnChanges, OnInit, QueryList, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
import {bypassSanitizationTrustHtml, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '@angular/core/src/sanitization/bypass';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
describe('host bindings', () => {
@ -175,6 +178,313 @@ describe('host bindings', () => {
});
});
describe('with synthetic (animations) props', () => {
it('should work when directive contains synthetic props', () => {
@Directive({
selector: '[animationPropDir]',
})
class AnimationPropDir {
@HostBinding('@myAnimation') myAnimation: string = 'color';
}
@Component({
selector: 'my-comp',
template: '<div animationPropDir>Some content</div>',
animations: [
trigger('myAnimation', [state('color', style({color: 'red'}))]),
],
})
class Comp {
}
TestBed.configureTestingModule({
declarations: [Comp, AnimationPropDir],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
const queryResult = fixture.debugElement.query(By.directive(AnimationPropDir));
expect(queryResult.nativeElement.style.color).toBe('red');
});
it('should work when directive contains synthetic props and directive is applied to a component',
() => {
@Directive({
selector: '[animationPropDir]',
})
class AnimationPropDir {
@HostBinding('@myAnimation') myAnimation: string = 'color';
}
@Component({
selector: 'my-comp',
template: 'Some content',
animations: [
trigger('myAnimation', [state('color', style({color: 'red'}))]),
],
})
class Comp {
}
@Component({
selector: 'app',
template: '<my-comp animationPropDir></my-comp>',
animations: [
trigger('myAnimation', [state('color', style({color: 'green'}))]),
],
})
class App {
}
TestBed.configureTestingModule({
declarations: [App, Comp, AnimationPropDir],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const queryResult = fixture.debugElement.query(By.directive(AnimationPropDir));
expect(queryResult.nativeElement.style.color).toBe('green');
});
it('should work when component contains synthetic props', () => {
@Component({
selector: 'my-comp',
template: '<div>Some content/div>',
animations: [
trigger('myAnimation', [state('color', style({color: 'red'}))]),
],
})
class Comp {
@HostBinding('@myAnimation') myAnimation: string = 'color';
}
TestBed.configureTestingModule({
declarations: [Comp],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
expect(fixture.nativeElement.style.color).toBe('red');
});
it('should work when child component contains synthetic props', () => {
@Component({
selector: 'my-comp',
template: '<div>Some content/div>',
animations: [
trigger('myAnimation', [state('color', style({color: 'red'}))]),
],
})
class Comp {
@HostBinding('@myAnimation') myAnimation: string = 'color';
}
@Component({
template: '<my-comp></my-comp>',
})
class App {
}
TestBed.configureTestingModule({
declarations: [App, Comp],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const queryResult = fixture.debugElement.query(By.directive(Comp));
expect(queryResult.nativeElement.style.color).toBe('red');
});
it('should work when component extends a directive that contains synthetic props', () => {
@Directive({
selector: 'animation-dir',
})
class AnimationDir {
@HostBinding('@myAnimation') myAnimation: string = 'color';
}
@Component({
selector: 'my-comp',
template: '<div>Some content</div>',
animations: [
trigger('myAnimation', [state('color', style({color: 'red'}))]),
],
})
class Comp extends AnimationDir {
}
TestBed.configureTestingModule({
declarations: [Comp, AnimationDir],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
expect(fixture.nativeElement.style.color).toBe('red');
});
it('should work when directive contains synthetic listeners', async () => {
const events: string[] = [];
@Directive({
selector: '[animationPropDir]',
})
class AnimationPropDir {
@HostBinding('@myAnimation') myAnimation: string = 'a';
@HostListener('@myAnimation.start')
onAnimationStart() {
events.push('@myAnimation.start');
}
@HostListener('@myAnimation.done')
onAnimationDone() {
events.push('@myAnimation.done');
}
}
@Component({
selector: 'my-comp',
template: '<div animationPropDir>Some content</div>',
animations: [
trigger('myAnimation', [state('a', style({color: 'yellow'})), transition('* => a', [])]),
],
})
class Comp {
}
TestBed.configureTestingModule({
declarations: [Comp, AnimationPropDir],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
await fixture.whenStable(); // wait for animations to complete
const queryResult = fixture.debugElement.query(By.directive(AnimationPropDir));
expect(queryResult.nativeElement.style.color).toBe('yellow');
expect(events).toEqual(['@myAnimation.start', '@myAnimation.done']);
});
it('should work when component contains synthetic listeners', async () => {
const events: string[] = [];
@Component({
selector: 'my-comp',
template: '<div>Some content</div>',
animations: [
trigger('myAnimation', [state('a', style({color: 'yellow'})), transition('* => a', [])]),
],
})
class Comp {
@HostBinding('@myAnimation') myAnimation: string = 'a';
@HostListener('@myAnimation.start')
onAnimationStart() {
events.push('@myAnimation.start');
}
@HostListener('@myAnimation.done')
onAnimationDone() {
events.push('@myAnimation.done');
}
}
TestBed.configureTestingModule({
declarations: [Comp],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
await fixture.whenStable(); // wait for animations to complete
expect(fixture.nativeElement.style.color).toBe('yellow');
expect(events).toEqual(['@myAnimation.start', '@myAnimation.done']);
});
it('should work when child component contains synthetic listeners', async () => {
const events: string[] = [];
@Component({
selector: 'my-comp',
template: '<div>Some content</div>',
animations: [
trigger('myAnimation', [state('a', style({color: 'yellow'})), transition('* => a', [])]),
],
})
class Comp {
@HostBinding('@myAnimation') myAnimation: string = 'a';
@HostListener('@myAnimation.start')
onAnimationStart() {
events.push('@myAnimation.start');
}
@HostListener('@myAnimation.done')
onAnimationDone() {
events.push('@myAnimation.done');
}
}
@Component({
template: '<my-comp></my-comp>',
})
class App {
}
TestBed.configureTestingModule({
declarations: [App, Comp],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
await fixture.whenStable(); // wait for animations to complete
const queryResult = fixture.debugElement.query(By.directive(Comp));
expect(queryResult.nativeElement.style.color).toBe('yellow');
expect(events).toEqual(['@myAnimation.start', '@myAnimation.done']);
});
it('should work when component extends a directive that contains synthetic listeners',
async () => {
const events: string[] = [];
@Directive({
selector: 'animation-dir',
})
class AnimationDir {
@HostBinding('@myAnimation') myAnimation: string = 'a';
@HostListener('@myAnimation.start')
onAnimationStart() {
events.push('@myAnimation.start');
}
@HostListener('@myAnimation.done')
onAnimationDone() {
events.push('@myAnimation.done');
}
}
@Component({
selector: 'my-comp',
template: '<div>Some content</div>',
animations: [
trigger(
'myAnimation', [state('a', style({color: 'yellow'})), transition('* => a', [])]),
],
})
class Comp extends AnimationDir {
}
TestBed.configureTestingModule({
declarations: [Comp],
imports: [NoopAnimationsModule],
});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
await fixture.whenStable(); // wait for animations to complete
expect(fixture.nativeElement.style.color).toBe('yellow');
expect(events).toEqual(['@myAnimation.start', '@myAnimation.done']);
});
});
describe('via @HostBinding', () => {
it('should render styling for parent and sub-classed components in order', () => {
@Component({

View File

@ -311,6 +311,9 @@
{
"name": "getContainerRenderParent"
},
{
"name": "getCurrentDirectiveIndex"
},
{
"name": "getDirectiveDef"
},
@ -587,6 +590,9 @@
{
"name": "setBindingRootForHostBindings"
},
{
"name": "setCurrentDirectiveIndex"
},
{
"name": "setCurrentQueryIndex"
},

View File

@ -455,6 +455,9 @@
{
"name": "setBindingRootForHostBindings"
},
{
"name": "setCurrentDirectiveIndex"
},
{
"name": "setCurrentQueryIndex"
},

View File

@ -587,6 +587,9 @@
{
"name": "getContextLView"
},
{
"name": "getCurrentDirectiveDef"
},
{
"name": "getCurrentDirectiveIndex"
},
@ -614,9 +617,6 @@
{
"name": "getFirstNativeNode"
},
{
"name": "getHostDirectiveDef"
},
{
"name": "getInjectableDef"
},
@ -1100,6 +1100,9 @@
{
"name": "setCheckNoChangesMode"
},
{
"name": "setCurrentDirectiveIndex"
},
{
"name": "setCurrentQueryIndex"
},