diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 9f60dd4556..203ffd0b54 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -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); } diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 387f6e1543..602e3470b0 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -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}; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index ceec9614be..e8efe7f798 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -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(); }); } diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 9a94298241..962320cdaf 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -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, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 30004f9d08..4d9865983b 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -44,6 +44,7 @@ export { elementEnd, elementProperty, componentHostSyntheticProperty, + componentHostSyntheticListener, elementStart, elementContainerStart, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 77ef8874a4..38b2541fb7 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -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( + 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( * 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( 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( 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]; +} diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 9951233f18..44d01358e5 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -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, diff --git a/packages/core/test/animation/animation_query_integration_spec.ts b/packages/core/test/animation/animation_query_integration_spec.ts index d3de042917..a2c8d90458 100644 --- a/packages/core/test/animation/animation_query_integration_spec.ts +++ b/packages/core/test/animation/animation_query_integration_spec.ts @@ -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: `
`, - 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', () => { diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 0de16a9c3f..38ec613887 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -950,6 +950,9 @@ { "name": "listener" }, + { + "name": "listenerInternal" + }, { "name": "loadInternal" },