fix(ivy): preventDefault when listener returns false (#22529)
Closes #22495 PR Close #22529
This commit is contained in:
		
							parent
							
								
									58932c7f38
								
							
						
					
					
						commit
						2c2b62f45f
					
				| @ -619,22 +619,24 @@ export function hostElement(rNode: RElement | null, def: ComponentDef<any>): LEl | |||||||
|  * and saves the subscription for later cleanup. |  * and saves the subscription for later cleanup. | ||||||
|  * |  * | ||||||
|  * @param eventName Name of the event |  * @param eventName Name of the event | ||||||
|  * @param listener The function to be called when event emits |  * @param listenerFn The function to be called when event emits | ||||||
|  * @param useCapture Whether or not to use capture in event listener. |  * @param useCapture Whether or not to use capture in event listener. | ||||||
|  */ |  */ | ||||||
| export function listener(eventName: string, listener: EventListener, useCapture = false): void { | export function listener( | ||||||
|  |     eventName: string, listenerFn: (e?: any) => any, useCapture = false): void { | ||||||
|   ngDevMode && assertPreviousIsParent(); |   ngDevMode && assertPreviousIsParent(); | ||||||
|   const node = previousOrParentNode; |   const node = previousOrParentNode; | ||||||
|   const native = node.native as RElement; |   const native = node.native as RElement; | ||||||
|   const wrappedListener = wrapListenerWithDirtyLogic(currentView, listener); |  | ||||||
| 
 | 
 | ||||||
|   // In order to match current behavior, native DOM event listeners must be added for all
 |   // In order to match current behavior, native DOM event listeners must be added for all
 | ||||||
|   // events (including outputs).
 |   // events (including outputs).
 | ||||||
|   const cleanupFns = cleanup || (cleanup = currentView.cleanup = []); |   const cleanupFns = cleanup || (cleanup = currentView.cleanup = []); | ||||||
|   if (isProceduralRenderer(renderer)) { |   if (isProceduralRenderer(renderer)) { | ||||||
|  |     const wrappedListener = wrapListenerWithDirtyLogic(currentView, listenerFn); | ||||||
|     const cleanupFn = renderer.listen(native, eventName, wrappedListener); |     const cleanupFn = renderer.listen(native, eventName, wrappedListener); | ||||||
|     cleanupFns.push(cleanupFn, null); |     cleanupFns.push(cleanupFn, null); | ||||||
|   } else { |   } else { | ||||||
|  |     const wrappedListener = wrapListenerWithDirtyAndDefault(currentView, listenerFn); | ||||||
|     native.addEventListener(eventName, wrappedListener, useCapture); |     native.addEventListener(eventName, wrappedListener, useCapture); | ||||||
|     cleanupFns.push(eventName, native, wrappedListener, useCapture); |     cleanupFns.push(eventName, native, wrappedListener, useCapture); | ||||||
|   } |   } | ||||||
| @ -649,7 +651,7 @@ export function listener(eventName: string, listener: EventListener, useCapture | |||||||
|   const outputs = tNode.outputs; |   const outputs = tNode.outputs; | ||||||
|   let outputData: PropertyAliasValue|undefined; |   let outputData: PropertyAliasValue|undefined; | ||||||
|   if (outputs && (outputData = outputs[eventName])) { |   if (outputs && (outputData = outputs[eventName])) { | ||||||
|     createOutput(outputData, listener); |     createOutput(outputData, listenerFn); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -1437,10 +1439,27 @@ export function markDirtyIfOnPush(node: LElementNode): void { | |||||||
|  * Wraps an event listener so its host view and its ancestor views will be marked dirty |  * Wraps an event listener so its host view and its ancestor views will be marked dirty | ||||||
|  * whenever the event fires. Necessary to support OnPush components. |  * whenever the event fires. Necessary to support OnPush components. | ||||||
|  */ |  */ | ||||||
| export function wrapListenerWithDirtyLogic(view: LView, listener: EventListener): EventListener { | export function wrapListenerWithDirtyLogic(view: LView, listenerFn: (e?: any) => any): (e: Event) => | ||||||
|  |     any { | ||||||
|  |   return function(e: any) { | ||||||
|  |     markViewDirty(view); | ||||||
|  |     return listenerFn(e); | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Wraps an event listener so its host view and its ancestor views will be marked dirty | ||||||
|  |  * whenever the event fires. Also wraps with preventDefault behavior. | ||||||
|  |  */ | ||||||
|  | export function wrapListenerWithDirtyAndDefault( | ||||||
|  |     view: LView, listenerFn: (e?: any) => any): EventListener { | ||||||
|   return function(e: Event) { |   return function(e: Event) { | ||||||
|     markViewDirty(view); |     markViewDirty(view); | ||||||
|     listener(e); |     if (listenerFn(e) === false) { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       // Necessary for legacy browsers that don't support preventDefault (e.g. IE)
 | ||||||
|  |       e.returnValue = false; | ||||||
|  |     } | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -51,4 +51,42 @@ describe('elements', () => { | |||||||
|     expect(toHtml(renderComponent(MyComponent))) |     expect(toHtml(renderComponent(MyComponent))) | ||||||
|         .toEqual('<div class="my-app" title="Hello">Hello <b>World</b>!</div>'); |         .toEqual('<div class="my-app" title="Hello">Hello <b>World</b>!</div>'); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   it('should support listeners', () => { | ||||||
|  |     type $ListenerComp$ = ListenerComp; | ||||||
|  | 
 | ||||||
|  |     @Component({ | ||||||
|  |       selector: 'listener-comp', | ||||||
|  |       template: | ||||||
|  |           `<button (click)="onClick()" (keypress)="onPress($event); onPress2($event)">Click</button>` | ||||||
|  |     }) | ||||||
|  |     class ListenerComp { | ||||||
|  |       onClick() {} | ||||||
|  |       onPress(e: Event) {} | ||||||
|  |       onPress2(e: Event) {} | ||||||
|  | 
 | ||||||
|  |       // NORMATIVE
 | ||||||
|  |       static ngComponentDef = $r3$.ɵdefineComponent({ | ||||||
|  |         type: ListenerComp, | ||||||
|  |         tag: 'listener-comp', | ||||||
|  |         factory: function ListenerComp_Factory() { return new ListenerComp(); }, | ||||||
|  |         template: function ListenerComp_Template(ctx: $ListenerComp$, cm: $boolean$) { | ||||||
|  |           if (cm) { | ||||||
|  |             $r3$.ɵE(0, 'button'); | ||||||
|  |             $r3$.ɵL('click', function ListenerComp_click_Handler() { return ctx.onClick(); }); | ||||||
|  |             $r3$.ɵL('keypress', function ListenerComp_keypress_Handler($event: $any$) { | ||||||
|  |               ctx.onPress($event); | ||||||
|  |               return ctx.onPress2($event); | ||||||
|  |             }); | ||||||
|  |             $r3$.ɵT(1, 'Click'); | ||||||
|  |             $r3$.ɵe(); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       // /NORMATIVE
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const listenerComp = renderComponent(ListenerComp); | ||||||
|  |     expect(toHtml(listenerComp)).toEqual('<button>Click</button>'); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ | |||||||
| import {defineComponent, defineDirective} from '../../src/render3/index'; | import {defineComponent, defineDirective} from '../../src/render3/index'; | ||||||
| import {container, containerRefreshEnd, containerRefreshStart, directiveRefresh, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, listener, text} from '../../src/render3/instructions'; | import {container, containerRefreshEnd, containerRefreshStart, directiveRefresh, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, listener, text} from '../../src/render3/instructions'; | ||||||
| 
 | 
 | ||||||
|  | import {getRendererFactory2} from './imported_renderer2'; | ||||||
| import {containerEl, renderComponent, renderToHtml} from './render_util'; | import {containerEl, renderComponent, renderToHtml} from './render_util'; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -29,7 +30,7 @@ describe('event listeners', () => { | |||||||
|         if (cm) { |         if (cm) { | ||||||
|           elementStart(0, 'button'); |           elementStart(0, 'button'); | ||||||
|           { |           { | ||||||
|             listener('click', function() { ctx.onClick(); }); |             listener('click', function() { return ctx.onClick(); }); | ||||||
|             text(1, 'Click me'); |             text(1, 'Click me'); | ||||||
|           } |           } | ||||||
|           elementEnd(); |           elementEnd(); | ||||||
| @ -43,6 +44,39 @@ describe('event listeners', () => { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   class PreventDefaultComp { | ||||||
|  |     handlerReturnValue: any = true; | ||||||
|  |     event: Event; | ||||||
|  | 
 | ||||||
|  |     onClick(e: any) { | ||||||
|  |       this.event = e; | ||||||
|  | 
 | ||||||
|  |       // stub preventDefault() to check whether it's called
 | ||||||
|  |       Object.defineProperty( | ||||||
|  |           this.event, 'preventDefault', | ||||||
|  |           {value: jasmine.createSpy('preventDefault'), writable: true}); | ||||||
|  | 
 | ||||||
|  |       return this.handlerReturnValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static ngComponentDef = defineComponent({ | ||||||
|  |       type: PreventDefaultComp, | ||||||
|  |       tag: 'prevent-default-comp', | ||||||
|  |       factory: () => new PreventDefaultComp(), | ||||||
|  |       /** <button (click)="onClick($event)">Click</button> */ | ||||||
|  |       template: (ctx: PreventDefaultComp, cm: boolean) => { | ||||||
|  |         if (cm) { | ||||||
|  |           elementStart(0, 'button'); | ||||||
|  |           { | ||||||
|  |             listener('click', function($event: any) { return ctx.onClick($event); }); | ||||||
|  |             text(1, 'Click'); | ||||||
|  |           } | ||||||
|  |           elementEnd(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   beforeEach(() => { comps = []; }); |   beforeEach(() => { comps = []; }); | ||||||
| 
 | 
 | ||||||
|   it('should call function on event emit', () => { |   it('should call function on event emit', () => { | ||||||
| @ -55,6 +89,38 @@ describe('event listeners', () => { | |||||||
|     expect(comp.counter).toEqual(2); |     expect(comp.counter).toEqual(2); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   it('should retain event handler return values using document', () => { | ||||||
|  |     const preventDefaultComp = renderComponent(PreventDefaultComp); | ||||||
|  |     const button = containerEl.querySelector('button') !; | ||||||
|  | 
 | ||||||
|  |     button.click(); | ||||||
|  |     expect(preventDefaultComp.event !.preventDefault).not.toHaveBeenCalled(); | ||||||
|  | 
 | ||||||
|  |     preventDefaultComp.handlerReturnValue = undefined; | ||||||
|  |     button.click(); | ||||||
|  |     expect(preventDefaultComp.event !.preventDefault).not.toHaveBeenCalled(); | ||||||
|  | 
 | ||||||
|  |     preventDefaultComp.handlerReturnValue = false; | ||||||
|  |     button.click(); | ||||||
|  |     expect(preventDefaultComp.event !.preventDefault).toHaveBeenCalled(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should retain event handler return values with renderer2', () => { | ||||||
|  |     const preventDefaultComp = renderComponent(PreventDefaultComp, getRendererFactory2(document)); | ||||||
|  |     const button = containerEl.querySelector('button') !; | ||||||
|  | 
 | ||||||
|  |     button.click(); | ||||||
|  |     expect(preventDefaultComp.event !.preventDefault).not.toHaveBeenCalled(); | ||||||
|  | 
 | ||||||
|  |     preventDefaultComp.handlerReturnValue = undefined; | ||||||
|  |     button.click(); | ||||||
|  |     expect(preventDefaultComp.event !.preventDefault).not.toHaveBeenCalled(); | ||||||
|  | 
 | ||||||
|  |     preventDefaultComp.handlerReturnValue = false; | ||||||
|  |     button.click(); | ||||||
|  |     expect(preventDefaultComp.event !.preventDefault).toHaveBeenCalled(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   it('should call function chain on event emit', () => { |   it('should call function chain on event emit', () => { | ||||||
|     /** <button (click)="onClick(); onClick2(); "> Click me </button> */ |     /** <button (click)="onClick(); onClick2(); "> Click me </button> */ | ||||||
|     function Template(ctx: any, cm: boolean) { |     function Template(ctx: any, cm: boolean) { | ||||||
| @ -63,7 +129,7 @@ describe('event listeners', () => { | |||||||
|         { |         { | ||||||
|           listener('click', function() { |           listener('click', function() { | ||||||
|             ctx.onClick(); |             ctx.onClick(); | ||||||
|             ctx.onClick2(); |             return ctx.onClick2(); | ||||||
|           }); |           }); | ||||||
|           text(1, 'Click me'); |           text(1, 'Click me'); | ||||||
|         } |         } | ||||||
| @ -96,7 +162,7 @@ describe('event listeners', () => { | |||||||
|       if (cm) { |       if (cm) { | ||||||
|         elementStart(0, 'button'); |         elementStart(0, 'button'); | ||||||
|         { |         { | ||||||
|           listener('click', function() { ctx.showing = !ctx.showing; }); |           listener('click', function() { return ctx.showing = !ctx.showing; }); | ||||||
|           text(1, 'Click me'); |           text(1, 'Click me'); | ||||||
|         } |         } | ||||||
|         elementEnd(); |         elementEnd(); | ||||||
| @ -131,7 +197,7 @@ describe('event listeners', () => { | |||||||
|           if (embeddedViewStart(1)) { |           if (embeddedViewStart(1)) { | ||||||
|             elementStart(0, 'button'); |             elementStart(0, 'button'); | ||||||
|             { |             { | ||||||
|               listener('click', function() { ctx.onClick(); }); |               listener('click', function() { return ctx.onClick(); }); | ||||||
|               text(1, 'Click me'); |               text(1, 'Click me'); | ||||||
|             } |             } | ||||||
|             elementEnd(); |             elementEnd(); | ||||||
| @ -170,7 +236,7 @@ describe('event listeners', () => { | |||||||
|         type: HostListenerDir, |         type: HostListenerDir, | ||||||
|         factory: function HostListenerDir_Factory() { |         factory: function HostListenerDir_Factory() { | ||||||
|           const $dir$ = new HostListenerDir(); |           const $dir$ = new HostListenerDir(); | ||||||
|           listener('click', function() { $dir$.onClick(); }); |           listener('click', function() { return $dir$.onClick(); }); | ||||||
|           return $dir$; |           return $dir$; | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
| @ -222,7 +288,7 @@ describe('event listeners', () => { | |||||||
|               if (embeddedViewStart(0)) { |               if (embeddedViewStart(0)) { | ||||||
|                 elementStart(0, 'button'); |                 elementStart(0, 'button'); | ||||||
|                 { |                 { | ||||||
|                   listener('click', function() { ctx.onClick(); }); |                   listener('click', function() { return ctx.onClick(); }); | ||||||
|                   text(1, 'Click'); |                   text(1, 'Click'); | ||||||
|                 } |                 } | ||||||
|                 elementEnd(); |                 elementEnd(); | ||||||
| @ -337,7 +403,7 @@ describe('event listeners', () => { | |||||||
|               if (embeddedViewStart(0)) { |               if (embeddedViewStart(0)) { | ||||||
|                 elementStart(0, 'button'); |                 elementStart(0, 'button'); | ||||||
|                 { |                 { | ||||||
|                   listener('click', function() { ctx.counter1++; }); |                   listener('click', function() { return ctx.counter1++; }); | ||||||
|                   text(1, 'Click'); |                   text(1, 'Click'); | ||||||
|                 } |                 } | ||||||
|                 elementEnd(); |                 elementEnd(); | ||||||
| @ -352,7 +418,7 @@ describe('event listeners', () => { | |||||||
|               if (embeddedViewStart(0)) { |               if (embeddedViewStart(0)) { | ||||||
|                 elementStart(0, 'button'); |                 elementStart(0, 'button'); | ||||||
|                 { |                 { | ||||||
|                   listener('click', function() { ctx.counter2++; }); |                   listener('click', function() { return ctx.counter2++; }); | ||||||
|                   text(1, 'Click'); |                   text(1, 'Click'); | ||||||
|                 } |                 } | ||||||
|                 elementEnd(); |                 elementEnd(); | ||||||
|  | |||||||
| @ -47,7 +47,7 @@ describe('outputs', () => { | |||||||
|       if (cm) { |       if (cm) { | ||||||
|         elementStart(0, ButtonToggle); |         elementStart(0, ButtonToggle); | ||||||
|         { |         { | ||||||
|           listener('change', function() { ctx.onChange(); }); |           listener('change', function() { return ctx.onChange(); }); | ||||||
|         } |         } | ||||||
|         elementEnd(); |         elementEnd(); | ||||||
|       } |       } | ||||||
| @ -72,8 +72,8 @@ describe('outputs', () => { | |||||||
|       if (cm) { |       if (cm) { | ||||||
|         elementStart(0, ButtonToggle); |         elementStart(0, ButtonToggle); | ||||||
|         { |         { | ||||||
|           listener('change', function() { ctx.onChange(); }); |           listener('change', function() { return ctx.onChange(); }); | ||||||
|           listener('reset', function() { ctx.onReset(); }); |           listener('reset', function() { return ctx.onReset(); }); | ||||||
|         } |         } | ||||||
|         elementEnd(); |         elementEnd(); | ||||||
|       } |       } | ||||||
| @ -99,7 +99,7 @@ describe('outputs', () => { | |||||||
|       if (cm) { |       if (cm) { | ||||||
|         elementStart(0, ButtonToggle); |         elementStart(0, ButtonToggle); | ||||||
|         { |         { | ||||||
|           listener('change', function() { ctx.counter++; }); |           listener('change', function() { return ctx.counter++; }); | ||||||
|         } |         } | ||||||
|         elementEnd(); |         elementEnd(); | ||||||
|       } |       } | ||||||
| @ -135,7 +135,7 @@ describe('outputs', () => { | |||||||
|           if (embeddedViewStart(0)) { |           if (embeddedViewStart(0)) { | ||||||
|             elementStart(0, ButtonToggle); |             elementStart(0, ButtonToggle); | ||||||
|             { |             { | ||||||
|               listener('change', function() { ctx.onChange(); }); |               listener('change', function() { return ctx.onChange(); }); | ||||||
|             } |             } | ||||||
|             elementEnd(); |             elementEnd(); | ||||||
|           } |           } | ||||||
| @ -187,7 +187,7 @@ describe('outputs', () => { | |||||||
|               if (embeddedViewStart(0)) { |               if (embeddedViewStart(0)) { | ||||||
|                 elementStart(0, ButtonToggle); |                 elementStart(0, ButtonToggle); | ||||||
|                 { |                 { | ||||||
|                   listener('change', function() { ctx.onChange(); }); |                   listener('change', function() { return ctx.onChange(); }); | ||||||
|                 } |                 } | ||||||
|                 elementEnd(); |                 elementEnd(); | ||||||
|               } |               } | ||||||
| @ -249,13 +249,13 @@ describe('outputs', () => { | |||||||
|           if (embeddedViewStart(0)) { |           if (embeddedViewStart(0)) { | ||||||
|             elementStart(0, 'button'); |             elementStart(0, 'button'); | ||||||
|             { |             { | ||||||
|               listener('click', function() { ctx.onClick(); }); |               listener('click', function() { return ctx.onClick(); }); | ||||||
|               text(1, 'Click me'); |               text(1, 'Click me'); | ||||||
|             } |             } | ||||||
|             elementEnd(); |             elementEnd(); | ||||||
|             elementStart(2, ButtonToggle); |             elementStart(2, ButtonToggle); | ||||||
|             { |             { | ||||||
|               listener('change', function() { ctx.onChange(); }); |               listener('change', function() { return ctx.onChange(); }); | ||||||
|             } |             } | ||||||
|             elementEnd(); |             elementEnd(); | ||||||
|             elementStart(4, DestroyComp); |             elementStart(4, DestroyComp); | ||||||
| @ -311,7 +311,7 @@ describe('outputs', () => { | |||||||
|       if (cm) { |       if (cm) { | ||||||
|         elementStart(0, 'button', null, [MyButton]); |         elementStart(0, 'button', null, [MyButton]); | ||||||
|         { |         { | ||||||
|           listener('click', function() { ctx.onClick(); }); |           listener('click', function() { return ctx.onClick(); }); | ||||||
|         } |         } | ||||||
|         elementEnd(); |         elementEnd(); | ||||||
|       } |       } | ||||||
| @ -336,7 +336,7 @@ describe('outputs', () => { | |||||||
|       if (cm) { |       if (cm) { | ||||||
|         elementStart(0, ButtonToggle, null, [OtherDir]); |         elementStart(0, ButtonToggle, null, [OtherDir]); | ||||||
|         { |         { | ||||||
|           listener('change', function() { ctx.onChange(); }); |           listener('change', function() { return ctx.onChange(); }); | ||||||
|         } |         } | ||||||
|         elementEnd(); |         elementEnd(); | ||||||
|       } |       } | ||||||
| @ -369,7 +369,7 @@ describe('outputs', () => { | |||||||
|       if (cm) { |       if (cm) { | ||||||
|         elementStart(0, ButtonToggle, null, [OtherDir]); |         elementStart(0, ButtonToggle, null, [OtherDir]); | ||||||
|         { |         { | ||||||
|           listener('change', function() { ctx.onChange(); }); |           listener('change', function() { return ctx.onChange(); }); | ||||||
|         } |         } | ||||||
|         elementEnd(); |         elementEnd(); | ||||||
|       } |       } | ||||||
| @ -403,7 +403,7 @@ describe('outputs', () => { | |||||||
|       if (cm) { |       if (cm) { | ||||||
|         elementStart(0, 'button'); |         elementStart(0, 'button'); | ||||||
|         { |         { | ||||||
|           listener('click', function() { ctx.onClick(); }); |           listener('click', function() { return ctx.onClick(); }); | ||||||
|           text(1, 'Click me'); |           text(1, 'Click me'); | ||||||
|         } |         } | ||||||
|         elementEnd(); |         elementEnd(); | ||||||
| @ -415,7 +415,7 @@ describe('outputs', () => { | |||||||
|           if (embeddedViewStart(0)) { |           if (embeddedViewStart(0)) { | ||||||
|             elementStart(0, ButtonToggle); |             elementStart(0, ButtonToggle); | ||||||
|             { |             { | ||||||
|               listener('change', function() { ctx.onChange(); }); |               listener('change', function() { return ctx.onChange(); }); | ||||||
|             } |             } | ||||||
|             elementEnd(); |             elementEnd(); | ||||||
|           } |           } | ||||||
| @ -426,7 +426,7 @@ describe('outputs', () => { | |||||||
|           if (embeddedViewStart(1)) { |           if (embeddedViewStart(1)) { | ||||||
|             elementStart(0, 'div', null, [OtherDir]); |             elementStart(0, 'div', null, [OtherDir]); | ||||||
|             { |             { | ||||||
|               listener('change', function() { ctx.onChange(); }); |               listener('change', function() { return ctx.onChange(); }); | ||||||
|             } |             } | ||||||
|             elementEnd(); |             elementEnd(); | ||||||
|           } |           } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user