fix(ivy): ensure animation component host listeners are rendered in the sub component (#28210)
Due to the fact that animations in Angular are defined in the component metadata, all animation trigger definitions are localized to the component and are inaccessible outside of it. Animation host listeners in Ivy are rendered in the context of the parent component, but the VE renders them differently. This patch ensures that animation host listeners are always registered in the sub component's renderer Jira issue: FW-943 Jira issue: FW-958 PR Close #28210
This commit is contained in:
parent
c1c87462fd
commit
6940992932
@ -341,8 +341,8 @@ describe('compiler compliance: styling', () => {
|
|||||||
hostBindings: function MyAnimDir_HostBindings(rf, ctx, elIndex) {
|
hostBindings: function MyAnimDir_HostBindings(rf, ctx, elIndex) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵallocHostVars(1);
|
$r3$.ɵallocHostVars(1);
|
||||||
$r3$.ɵlistener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); });
|
$r3$.ɵcomponentHostSyntheticListener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); });
|
||||||
$r3$.ɵlistener("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); });
|
$r3$.ɵcomponentHostSyntheticListener("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); });
|
||||||
} if (rf & 2) {
|
} if (rf & 2) {
|
||||||
$r3$.ɵcomponentHostSyntheticProperty(elIndex, "@myAnim", $r3$.ɵbind(ctx.myAnimState), null, true);
|
$r3$.ɵcomponentHostSyntheticProperty(elIndex, "@myAnim", $r3$.ɵbind(ctx.myAnimState), null, true);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,9 @@ export class Identifiers {
|
|||||||
static componentHostSyntheticProperty:
|
static componentHostSyntheticProperty:
|
||||||
o.ExternalReference = {name: 'ɵcomponentHostSyntheticProperty', moduleName: CORE};
|
o.ExternalReference = {name: 'ɵcomponentHostSyntheticProperty', moduleName: CORE};
|
||||||
|
|
||||||
|
static componentHostSyntheticListener:
|
||||||
|
o.ExternalReference = {name: 'ɵcomponentHostSyntheticListener', moduleName: CORE};
|
||||||
|
|
||||||
static elementAttribute: o.ExternalReference = {name: 'ɵelementAttribute', moduleName: CORE};
|
static elementAttribute: o.ExternalReference = {name: 'ɵelementAttribute', moduleName: CORE};
|
||||||
|
|
||||||
static elementClassProp: o.ExternalReference = {name: 'ɵelementClassProp', moduleName: CORE};
|
static elementClassProp: o.ExternalReference = {name: 'ɵelementClassProp', moduleName: CORE};
|
||||||
|
@ -839,7 +839,9 @@ function createHostListeners(
|
|||||||
meta.name && bindingName ? `${meta.name}_${bindingFnName}_HostBindingHandler` : null;
|
meta.name && bindingName ? `${meta.name}_${bindingFnName}_HostBindingHandler` : null;
|
||||||
const params = prepareEventListenerParameters(
|
const params = prepareEventListenerParameters(
|
||||||
BoundEvent.fromParsedEvent(binding), bindingContext, handlerName);
|
BoundEvent.fromParsedEvent(binding), bindingContext, handlerName);
|
||||||
return o.importExpr(R3.listener).callFn(params).toStmt();
|
const instruction =
|
||||||
|
binding.type == ParsedEventType.Animation ? R3.componentHostSyntheticListener : R3.listener;
|
||||||
|
return o.importExpr(instruction).callFn(params).toStmt();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +83,7 @@ export {
|
|||||||
elementEnd as ɵelementEnd,
|
elementEnd as ɵelementEnd,
|
||||||
elementProperty as ɵelementProperty,
|
elementProperty as ɵelementProperty,
|
||||||
componentHostSyntheticProperty as ɵcomponentHostSyntheticProperty,
|
componentHostSyntheticProperty as ɵcomponentHostSyntheticProperty,
|
||||||
|
componentHostSyntheticListener as ɵcomponentHostSyntheticListener,
|
||||||
projectionDef as ɵprojectionDef,
|
projectionDef as ɵprojectionDef,
|
||||||
reference as ɵreference,
|
reference as ɵreference,
|
||||||
enableBindings as ɵenableBindings,
|
enableBindings as ɵenableBindings,
|
||||||
|
@ -44,6 +44,7 @@ export {
|
|||||||
elementEnd,
|
elementEnd,
|
||||||
elementProperty,
|
elementProperty,
|
||||||
componentHostSyntheticProperty,
|
componentHostSyntheticProperty,
|
||||||
|
componentHostSyntheticListener,
|
||||||
elementStart,
|
elementStart,
|
||||||
|
|
||||||
elementContainerStart,
|
elementContainerStart,
|
||||||
|
@ -876,6 +876,38 @@ export function locateHostElement(
|
|||||||
export function listener(
|
export function listener(
|
||||||
eventName: string, listenerFn: (e?: any) => any, useCapture = false,
|
eventName: string, listenerFn: (e?: any) => any, useCapture = false,
|
||||||
eventTargetResolver?: GlobalTargetResolver): void {
|
eventTargetResolver?: GlobalTargetResolver): void {
|
||||||
|
listenerInternal(eventName, listenerFn, useCapture, eventTargetResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a synthetic host listener (e.g. `(@foo.start)`) on a component.
|
||||||
|
*
|
||||||
|
* This instruction is for compatibility purposes and is designed to ensure that a
|
||||||
|
* synthetic host listener (e.g. `@HostListener('@foo.start')`) properly gets rendered
|
||||||
|
* in the component's renderer. Normally all host listeners are evaluated with the
|
||||||
|
* parent component's renderer, but, in the case of animation @triggers, they need
|
||||||
|
* to be evaluated with the sub component's renderer (because that's where the
|
||||||
|
* animation triggers are defined).
|
||||||
|
*
|
||||||
|
* Do not use this instruction as a replacement for `listener`. This instruction
|
||||||
|
* only exists to ensure compatibility with the ViewEngine's host binding behavior.
|
||||||
|
*
|
||||||
|
* @param eventName Name of the event
|
||||||
|
* @param listenerFn The function to be called when event emits
|
||||||
|
* @param useCapture Whether or not to use capture in event listener
|
||||||
|
* @param eventTargetResolver Function that returns global target information in case this listener
|
||||||
|
* should be attached to a global object like window, document or body
|
||||||
|
*/
|
||||||
|
export function componentHostSyntheticListener<T>(
|
||||||
|
eventName: string, listenerFn: (e?: any) => any, useCapture = false,
|
||||||
|
eventTargetResolver?: GlobalTargetResolver): void {
|
||||||
|
listenerInternal(eventName, listenerFn, useCapture, eventTargetResolver, loadComponentRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenerInternal(
|
||||||
|
eventName: string, listenerFn: (e?: any) => any, useCapture = false,
|
||||||
|
eventTargetResolver?: GlobalTargetResolver,
|
||||||
|
loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void {
|
||||||
const lView = getLView();
|
const lView = getLView();
|
||||||
const tNode = getPreviousOrParentTNode();
|
const tNode = getPreviousOrParentTNode();
|
||||||
const tView = lView[TVIEW];
|
const tView = lView[TVIEW];
|
||||||
@ -890,7 +922,7 @@ export function listener(
|
|||||||
const resolved = eventTargetResolver ? eventTargetResolver(native) : {} as any;
|
const resolved = eventTargetResolver ? eventTargetResolver(native) : {} as any;
|
||||||
const target = resolved.target || native;
|
const target = resolved.target || native;
|
||||||
ngDevMode && ngDevMode.rendererAddEventListener++;
|
ngDevMode && ngDevMode.rendererAddEventListener++;
|
||||||
const renderer = lView[RENDERER];
|
const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER];
|
||||||
const lCleanup = getCleanup(lView);
|
const lCleanup = getCleanup(lView);
|
||||||
const lCleanupIndex = lCleanup.length;
|
const lCleanupIndex = lCleanup.length;
|
||||||
let useCaptureOrSubIdx: boolean|number = useCapture;
|
let useCaptureOrSubIdx: boolean|number = useCapture;
|
||||||
@ -1073,7 +1105,7 @@ export function elementProperty<T>(
|
|||||||
* synthetic host binding (e.g. `@HostBinding('@foo')`) properly gets rendered in
|
* synthetic host binding (e.g. `@HostBinding('@foo')`) properly gets rendered in
|
||||||
* the component's renderer. Normally all host bindings are evaluated with the parent
|
* the component's renderer. Normally all host bindings are evaluated with the parent
|
||||||
* component's renderer, but, in the case of animation @triggers, they need to be
|
* component's renderer, but, in the case of animation @triggers, they need to be
|
||||||
* evaluated with the sub components renderer (because that's where the animation
|
* evaluated with the sub component's renderer (because that's where the animation
|
||||||
* triggers are defined).
|
* triggers are defined).
|
||||||
*
|
*
|
||||||
* Do not use this instruction as a replacement for `elementProperty`. This instruction
|
* Do not use this instruction as a replacement for `elementProperty`. This instruction
|
||||||
@ -1093,11 +1125,6 @@ export function componentHostSyntheticProperty<T>(
|
|||||||
elementPropertyInternal(index, propName, value, sanitizer, nativeOnly, loadComponentRenderer);
|
elementPropertyInternal(index, propName, value, sanitizer, nativeOnly, loadComponentRenderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadComponentRenderer(tNode: TNode, lView: LView): Renderer3 {
|
|
||||||
const componentLView = lView[tNode.index] as LView;
|
|
||||||
return componentLView[RENDERER];
|
|
||||||
}
|
|
||||||
|
|
||||||
function elementPropertyInternal<T>(
|
function elementPropertyInternal<T>(
|
||||||
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
|
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
|
||||||
nativeOnly?: boolean,
|
nativeOnly?: boolean,
|
||||||
@ -3060,3 +3087,12 @@ function getCleanup(view: LView): any[] {
|
|||||||
function getTViewCleanup(view: LView): any[] {
|
function getTViewCleanup(view: LView): any[] {
|
||||||
return view[TVIEW].cleanup || (view[TVIEW].cleanup = []);
|
return view[TVIEW].cleanup || (view[TVIEW].cleanup = []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are cases where the sub component's renderer needs to be included
|
||||||
|
* instead of the current renderer (see the componentSyntheticHost* instructions).
|
||||||
|
*/
|
||||||
|
function loadComponentRenderer(tNode: TNode, lView: LView): Renderer3 {
|
||||||
|
const componentLView = lView[tNode.index] as LView;
|
||||||
|
return componentLView[RENDERER];
|
||||||
|
}
|
||||||
|
@ -78,6 +78,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
|
|||||||
'ɵprojection': r3.projection,
|
'ɵprojection': r3.projection,
|
||||||
'ɵelementProperty': r3.elementProperty,
|
'ɵelementProperty': r3.elementProperty,
|
||||||
'ɵcomponentHostSyntheticProperty': r3.componentHostSyntheticProperty,
|
'ɵcomponentHostSyntheticProperty': r3.componentHostSyntheticProperty,
|
||||||
|
'ɵcomponentHostSyntheticListener': r3.componentHostSyntheticListener,
|
||||||
'ɵpipeBind1': r3.pipeBind1,
|
'ɵpipeBind1': r3.pipeBind1,
|
||||||
'ɵpipeBind2': r3.pipeBind2,
|
'ɵpipeBind2': r3.pipeBind2,
|
||||||
'ɵpipeBind3': r3.pipeBind3,
|
'ɵpipeBind3': r3.pipeBind3,
|
||||||
|
@ -2238,84 +2238,82 @@ import {HostListener} from '../../src/metadata/directives';
|
|||||||
expect(p3.element.classList.contains('parent1')).toBeTruthy();
|
expect(p3.element.classList.contains('parent1')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
fixmeIvy(
|
it('should emulate a leave animation on the nearest sub host elements when a parent is removed',
|
||||||
'FW-943 - Fix final `unknown` issue in `animation_query_integration_spec.ts` once #28162 lands')
|
fakeAsync(() => {
|
||||||
.it('should emulate a leave animation on the nearest sub host elements when a parent is removed',
|
@Component({
|
||||||
fakeAsync(() => {
|
selector: 'ani-cmp',
|
||||||
@Component({
|
template: `
|
||||||
selector: 'ani-cmp',
|
|
||||||
template: `
|
|
||||||
<div @parent *ngIf="exp" class="parent1" #parent>
|
<div @parent *ngIf="exp" class="parent1" #parent>
|
||||||
<child-cmp #child @leave (@leave.start)="animateStart($event)"></child-cmp>
|
<child-cmp #child @leave (@leave.start)="animateStart($event)"></child-cmp>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
animations: [
|
animations: [
|
||||||
trigger(
|
trigger(
|
||||||
'leave',
|
'leave',
|
||||||
[
|
[
|
||||||
transition(':leave', [animate(1000, style({color: 'gold'}))]),
|
transition(':leave', [animate(1000, style({color: 'gold'}))]),
|
||||||
]),
|
]),
|
||||||
trigger(
|
trigger(
|
||||||
'parent',
|
'parent',
|
||||||
[
|
[
|
||||||
transition(':leave', [query(':leave', animateChild())]),
|
transition(':leave', [query(':leave', animateChild())]),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
class ParentCmp {
|
class ParentCmp {
|
||||||
public exp: boolean = true;
|
public exp: boolean = true;
|
||||||
@ViewChild('child') public childElm: any;
|
@ViewChild('child') public childElm: any;
|
||||||
|
|
||||||
public childEvent: any;
|
public childEvent: any;
|
||||||
|
|
||||||
animateStart(event: any) {
|
animateStart(event: any) {
|
||||||
if (event.toState == 'void') {
|
if (event.toState == 'void') {
|
||||||
this.childEvent = event;
|
this.childEvent = event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'child-cmp',
|
selector: 'child-cmp',
|
||||||
template: '...',
|
template: '...',
|
||||||
animations: [
|
animations: [
|
||||||
trigger(
|
trigger(
|
||||||
'child',
|
'child',
|
||||||
[
|
[
|
||||||
transition(':leave', [animate(1000, style({color: 'gold'}))]),
|
transition(':leave', [animate(1000, style({color: 'gold'}))]),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
class ChildCmp {
|
class ChildCmp {
|
||||||
public childEvent: any;
|
public childEvent: any;
|
||||||
|
|
||||||
@HostBinding('@child') public animate = true;
|
@HostBinding('@child') public animate = true;
|
||||||
|
|
||||||
@HostListener('@child.start', ['$event'])
|
@HostListener('@child.start', ['$event'])
|
||||||
animateStart(event: any) {
|
animateStart(event: any) {
|
||||||
if (event.toState == 'void') {
|
if (event.toState == 'void') {
|
||||||
this.childEvent = event;
|
this.childEvent = event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
|
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
|
||||||
const fixture = TestBed.createComponent(ParentCmp);
|
const fixture = TestBed.createComponent(ParentCmp);
|
||||||
const cmp = fixture.componentInstance;
|
const cmp = fixture.componentInstance;
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const childCmp = cmp.childElm;
|
const childCmp = cmp.childElm;
|
||||||
|
|
||||||
cmp.exp = false;
|
cmp.exp = false;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
flushMicrotasks();
|
flushMicrotasks();
|
||||||
|
|
||||||
expect(cmp.childEvent.toState).toEqual('void');
|
expect(cmp.childEvent.toState).toEqual('void');
|
||||||
expect(cmp.childEvent.totalTime).toEqual(1000);
|
expect(cmp.childEvent.totalTime).toEqual(1000);
|
||||||
expect(childCmp.childEvent.toState).toEqual('void');
|
expect(childCmp.childEvent.toState).toEqual('void');
|
||||||
expect(childCmp.childEvent.totalTime).toEqual(1000);
|
expect(childCmp.childEvent.totalTime).toEqual(1000);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should emulate a leave animation on a sub component\'s inner elements when a parent leave animation occurs with animateChild',
|
it('should emulate a leave animation on a sub component\'s inner elements when a parent leave animation occurs with animateChild',
|
||||||
() => {
|
() => {
|
||||||
|
@ -950,6 +950,9 @@
|
|||||||
{
|
{
|
||||||
"name": "listener"
|
"name": "listener"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "listenerInternal"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "loadInternal"
|
"name": "loadInternal"
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user