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) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵallocHostVars(1);
|
||||
$r3$.ɵlistener("@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.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); });
|
||||
$r3$.ɵcomponentHostSyntheticListener("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); });
|
||||
} if (rf & 2) {
|
||||
$r3$.ɵcomponentHostSyntheticProperty(elIndex, "@myAnim", $r3$.ɵbind(ctx.myAnimState), null, true);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,9 @@ export class Identifiers {
|
|||
static componentHostSyntheticProperty:
|
||||
o.ExternalReference = {name: 'ɵcomponentHostSyntheticProperty', moduleName: CORE};
|
||||
|
||||
static componentHostSyntheticListener:
|
||||
o.ExternalReference = {name: 'ɵcomponentHostSyntheticListener', moduleName: CORE};
|
||||
|
||||
static elementAttribute: o.ExternalReference = {name: 'ɵelementAttribute', moduleName: CORE};
|
||||
|
||||
static elementClassProp: o.ExternalReference = {name: 'ɵelementClassProp', moduleName: CORE};
|
||||
|
|
|
@ -839,7 +839,9 @@ function createHostListeners(
|
|||
meta.name && bindingName ? `${meta.name}_${bindingFnName}_HostBindingHandler` : null;
|
||||
const params = prepareEventListenerParameters(
|
||||
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,
|
||||
elementProperty as ɵelementProperty,
|
||||
componentHostSyntheticProperty as ɵcomponentHostSyntheticProperty,
|
||||
componentHostSyntheticListener as ɵcomponentHostSyntheticListener,
|
||||
projectionDef as ɵprojectionDef,
|
||||
reference as ɵreference,
|
||||
enableBindings as ɵenableBindings,
|
||||
|
|
|
@ -44,6 +44,7 @@ export {
|
|||
elementEnd,
|
||||
elementProperty,
|
||||
componentHostSyntheticProperty,
|
||||
componentHostSyntheticListener,
|
||||
elementStart,
|
||||
|
||||
elementContainerStart,
|
||||
|
|
|
@ -876,6 +876,38 @@ export function locateHostElement(
|
|||
export function listener(
|
||||
eventName: string, listenerFn: (e?: any) => any, useCapture = false,
|
||||
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 tNode = getPreviousOrParentTNode();
|
||||
const tView = lView[TVIEW];
|
||||
|
@ -890,7 +922,7 @@ export function listener(
|
|||
const resolved = eventTargetResolver ? eventTargetResolver(native) : {} as any;
|
||||
const target = resolved.target || native;
|
||||
ngDevMode && ngDevMode.rendererAddEventListener++;
|
||||
const renderer = lView[RENDERER];
|
||||
const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER];
|
||||
const lCleanup = getCleanup(lView);
|
||||
const lCleanupIndex = lCleanup.length;
|
||||
let useCaptureOrSubIdx: boolean|number = useCapture;
|
||||
|
@ -1073,7 +1105,7 @@ export function elementProperty<T>(
|
|||
* synthetic host binding (e.g. `@HostBinding('@foo')`) properly gets rendered in
|
||||
* 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
|
||||
* 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).
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
|
||||
function loadComponentRenderer(tNode: TNode, lView: LView): Renderer3 {
|
||||
const componentLView = lView[tNode.index] as LView;
|
||||
return componentLView[RENDERER];
|
||||
}
|
||||
|
||||
function elementPropertyInternal<T>(
|
||||
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
|
||||
nativeOnly?: boolean,
|
||||
|
@ -3060,3 +3087,12 @@ function getCleanup(view: LView): any[] {
|
|||
function getTViewCleanup(view: LView): any[] {
|
||||
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,
|
||||
'ɵelementProperty': r3.elementProperty,
|
||||
'ɵcomponentHostSyntheticProperty': r3.componentHostSyntheticProperty,
|
||||
'ɵcomponentHostSyntheticListener': r3.componentHostSyntheticListener,
|
||||
'ɵpipeBind1': r3.pipeBind1,
|
||||
'ɵpipeBind2': r3.pipeBind2,
|
||||
'ɵpipeBind3': r3.pipeBind3,
|
||||
|
|
|
@ -2238,84 +2238,82 @@ import {HostListener} from '../../src/metadata/directives';
|
|||
expect(p3.element.classList.contains('parent1')).toBeTruthy();
|
||||
});
|
||||
|
||||
fixmeIvy(
|
||||
'FW-943 - Fix final `unknown` issue in `animation_query_integration_spec.ts` once #28162 lands')
|
||||
.it('should emulate a leave animation on the nearest sub host elements when a parent is removed',
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'ani-cmp',
|
||||
template: `
|
||||
it('should emulate a leave animation on the nearest sub host elements when a parent is removed',
|
||||
fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'ani-cmp',
|
||||
template: `
|
||||
<div @parent *ngIf="exp" class="parent1" #parent>
|
||||
<child-cmp #child @leave (@leave.start)="animateStart($event)"></child-cmp>
|
||||
</div>
|
||||
`,
|
||||
animations: [
|
||||
trigger(
|
||||
'leave',
|
||||
[
|
||||
transition(':leave', [animate(1000, style({color: 'gold'}))]),
|
||||
]),
|
||||
trigger(
|
||||
'parent',
|
||||
[
|
||||
transition(':leave', [query(':leave', animateChild())]),
|
||||
]),
|
||||
]
|
||||
})
|
||||
class ParentCmp {
|
||||
public exp: boolean = true;
|
||||
@ViewChild('child') public childElm: any;
|
||||
animations: [
|
||||
trigger(
|
||||
'leave',
|
||||
[
|
||||
transition(':leave', [animate(1000, style({color: 'gold'}))]),
|
||||
]),
|
||||
trigger(
|
||||
'parent',
|
||||
[
|
||||
transition(':leave', [query(':leave', animateChild())]),
|
||||
]),
|
||||
]
|
||||
})
|
||||
class ParentCmp {
|
||||
public exp: boolean = true;
|
||||
@ViewChild('child') public childElm: any;
|
||||
|
||||
public childEvent: any;
|
||||
public childEvent: any;
|
||||
|
||||
animateStart(event: any) {
|
||||
if (event.toState == 'void') {
|
||||
this.childEvent = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
animateStart(event: any) {
|
||||
if (event.toState == 'void') {
|
||||
this.childEvent = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'child-cmp',
|
||||
template: '...',
|
||||
animations: [
|
||||
trigger(
|
||||
'child',
|
||||
[
|
||||
transition(':leave', [animate(1000, style({color: 'gold'}))]),
|
||||
]),
|
||||
]
|
||||
})
|
||||
class ChildCmp {
|
||||
public childEvent: any;
|
||||
@Component({
|
||||
selector: 'child-cmp',
|
||||
template: '...',
|
||||
animations: [
|
||||
trigger(
|
||||
'child',
|
||||
[
|
||||
transition(':leave', [animate(1000, style({color: 'gold'}))]),
|
||||
]),
|
||||
]
|
||||
})
|
||||
class ChildCmp {
|
||||
public childEvent: any;
|
||||
|
||||
@HostBinding('@child') public animate = true;
|
||||
@HostBinding('@child') public animate = true;
|
||||
|
||||
@HostListener('@child.start', ['$event'])
|
||||
animateStart(event: any) {
|
||||
if (event.toState == 'void') {
|
||||
this.childEvent = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
@HostListener('@child.start', ['$event'])
|
||||
animateStart(event: any) {
|
||||
if (event.toState == 'void') {
|
||||
this.childEvent = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
|
||||
const fixture = TestBed.createComponent(ParentCmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
|
||||
const fixture = TestBed.createComponent(ParentCmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.detectChanges();
|
||||
|
||||
const childCmp = cmp.childElm;
|
||||
const childCmp = cmp.childElm;
|
||||
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
flushMicrotasks();
|
||||
|
||||
expect(cmp.childEvent.toState).toEqual('void');
|
||||
expect(cmp.childEvent.totalTime).toEqual(1000);
|
||||
expect(childCmp.childEvent.toState).toEqual('void');
|
||||
expect(childCmp.childEvent.totalTime).toEqual(1000);
|
||||
}));
|
||||
expect(cmp.childEvent.toState).toEqual('void');
|
||||
expect(cmp.childEvent.totalTime).toEqual(1000);
|
||||
expect(childCmp.childEvent.toState).toEqual('void');
|
||||
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',
|
||||
() => {
|
||||
|
|
|
@ -950,6 +950,9 @@
|
|||
{
|
||||
"name": "listener"
|
||||
},
|
||||
{
|
||||
"name": "listenerInternal"
|
||||
},
|
||||
{
|
||||
"name": "loadInternal"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue