Skip to content

Commit

Permalink
fix(engine): patching events from lwc to be deterministic (#870)
Browse files Browse the repository at this point in the history
* fix(engine): patching events from lwc to be deterministic

* fix(engine): PR #870 feedback

* fix(engine): new license headers
  • Loading branch information
caridy authored and ekashida committed Jan 10, 2019
1 parent 88c1d92 commit 8d3fc9f
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 20 deletions.
16 changes: 14 additions & 2 deletions packages/@lwc/engine/src/env/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import { hasOwnProperty, getOwnPropertyDescriptor } from "../shared/language";

const {
addEventListener,
removeEventListener,
hasAttribute,
getAttribute,
getAttributeNS,
Expand All @@ -24,6 +22,20 @@ const {
getElementsByTagNameNS,
} = Element.prototype;

let {
addEventListener,
removeEventListener,
} = Element.prototype;

/**
* This trick to try to pick up the __lwcOriginal__ out of the intrinsic is to please
* jsdom, who usually reuse intrinsic between different document.
*/
// @ts-ignore jsdom
addEventListener = addEventListener.__lwcOriginal__ || addEventListener;
// @ts-ignore jsdom
removeEventListener = removeEventListener.__lwcOriginal__ || removeEventListener;

const innerHTMLSetter: (this: Element, s: string) => void = hasOwnProperty.call(Element.prototype, 'innerHTML') ?
getOwnPropertyDescriptor(Element.prototype, 'innerHTML')!.set! :
getOwnPropertyDescriptor(HTMLElement.prototype, 'innerHTML')!.set!; // IE11
Expand Down
16 changes: 16 additions & 0 deletions packages/@lwc/engine/src/env/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,23 @@ if (typeof MO === 'undefined') {
const MutationObserver = MO;
const MutationObserverObserve = MutationObserver.prototype.observe;

let {
addEventListener: windowAddEventListener,
removeEventListener: windowRemoveEventListener,
} = window;

/**
* This trick to try to pick up the __lwcOriginal__ out of the intrinsic is to please
* jsdom, who usually reuse intrinsic between different document.
*/
// @ts-ignore jsdom
windowAddEventListener = windowAddEventListener.__lwcOriginal__ || windowAddEventListener;
// @ts-ignore jsdom
windowRemoveEventListener = windowRemoveEventListener.__lwcOriginal__ || windowRemoveEventListener;

export {
MutationObserver,
MutationObserverObserve,
windowAddEventListener,
windowRemoveEventListener,
};
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,10 @@ describe('events', () => {
render() {
return rootHTML;
}
handleClick(evt) {
// event handler is here to trigger patching of the event
}
}
const rootHTML = compileTemplate(`
<template>
<div onclick={handleClick}></div>
<div></div>
</template>
`);

Expand Down
14 changes: 9 additions & 5 deletions packages/@lwc/engine/src/faux-shadow/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import assert from "../shared/assert";
import {
addEventListener,
removeEventListener,
} from "../env/element";
import {
compareDocumentPosition,
DOCUMENT_POSITION_CONTAINED_BY,
Expand All @@ -20,6 +16,15 @@ import { eventCurrentTargetGetter, eventTargetGetter } from "../env/dom";
import { pathComposer } from "./../3rdparty/polymer/path-composer";
import { retarget } from "./../3rdparty/polymer/retarget";

import "../polyfills/event-listener/main";

// intentionally extracting the patched addEventListener and removeEventListener from Node.prototype
// due to the issues with JSDOM patching hazard.
const {
addEventListener,
removeEventListener,
} = Node.prototype;

interface WrappedListener extends EventListener {
placement: EventListenerContext;
original: EventListener;
Expand Down Expand Up @@ -202,7 +207,6 @@ function domListener(evt: Event) {
enumerable: true,
configurable: true,
});
patchEvent(evt);
// in case a listener adds or removes other listeners during invocation
const bookkeeping: WrappedListener[] = ArraySlice.call(listeners);

Expand Down
5 changes: 1 addition & 4 deletions packages/@lwc/engine/src/framework/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
createTextHook,
createCommentHook,
} from "./hooks";
import { markAsDynamicChildren, patchEvent } from "./patch";
import { markAsDynamicChildren } from "./patch";
import {
isNativeShadowRootAvailable,
} from "../env/dom";
Expand Down Expand Up @@ -547,9 +547,6 @@ export function b(fn: EventListener): EventListener {
}
const vm: VM = vmBeingRendered;
return function(event: Event) {
if (vm.fallback) {
patchEvent(event);
}
invokeEventListener(vm, fn, vm.component, event);
};
}
Expand Down
5 changes: 0 additions & 5 deletions packages/@lwc/engine/src/framework/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { VNodes } from "../3rdparty/snabbdom/types";
import { patchEvent } from "../faux-shadow/faux";
import { tagNameGetter } from "../env/element";
import { updateDynamicChildren, updateStaticChildren } from "../3rdparty/snabbdom/snabbdom";
import { setPrototypeOf, create, isUndefined, isTrue } from "../shared/language";
Expand Down Expand Up @@ -99,7 +98,3 @@ export function patchCustomElementProto(elm: HTMLElement, options: { def: Compon
setPrototypeOf(elm, patchedBridge.prototype);
setCSSToken(elm, shadowAttribute);
}

export {
patchEvent,
};
6 changes: 6 additions & 0 deletions packages/@lwc/engine/src/polyfills/event-listener/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# event-listener polyfill

This polyfill is needed to support re-targeting for synthetic shadow dom.

It will patch addEventListener and removeEventListener to make sure that whenever you are listening for an event at any level, that event is patched accordingly but only if the event is triggered from within a shadow root. If the event is not coming from a synthetic shadow, the event
don't need to be patched.
9 changes: 9 additions & 0 deletions packages/@lwc/engine/src/polyfills/event-listener/detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
export default function detect(): boolean {
return true;
}
12 changes: 12 additions & 0 deletions packages/@lwc/engine/src/polyfills/event-listener/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import detect from './detect';
import apply from './polyfill';

if (detect()) {
apply();
}
104 changes: 104 additions & 0 deletions packages/@lwc/engine/src/polyfills/event-listener/polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import {
windowRemoveEventListener as nativeWindowRemoveEventListener,
windowAddEventListener as nativeWindowAddEventListener,
} from '../../env/window';
import {
removeEventListener as nativeRemoveEventListener,
addEventListener as nativeAddEventListener,
} from '../../env/element';
import { eventTargetGetter } from '../../env/dom';
import { DOCUMENT_POSITION_CONTAINED_BY, compareDocumentPosition } from '../../env/node';
import { getNodeOwnerKey } from '../../faux-shadow/node';
import { patchEvent } from '../../faux-shadow/events';

function doesEventNeedsPatch(e: Event): boolean {
const originalTarget = eventTargetGetter.call(e);
if (originalTarget instanceof Node) {
if ((compareDocumentPosition.call(document, originalTarget) & DOCUMENT_POSITION_CONTAINED_BY) !== 0 && getNodeOwnerKey(originalTarget)) {
return true;
}
}
return false;
}

function getEventListenerWrapper(fnOrObj): EventListener | null {
let wrapperFn: EventListener | null = null;
try {
wrapperFn = fnOrObj.$$lwcEventWrapper$$;
if (!wrapperFn) {
wrapperFn = fnOrObj.$$lwcEventWrapper$$ = function(this: EventTarget, e: Event) {
// we don't want to patch every event, only when the original target is coming
// from inside a synthetic shadow
if (doesEventNeedsPatch(e)) {
patchEvent(e);
}
return fnOrObj.call(this, e);
};
}
} catch (e) { /** ignore */ }
return wrapperFn;
}

function windowAddEventListener(this: EventTarget, type, fnOrObj, optionsOrCapture) {
const handlerType = typeof fnOrObj;
// bail if `fnOrObj` is not a function, not an object
if (handlerType !== 'function' && handlerType !== 'object') {
return;
}
// bail if `fnOrObj` is an object without a `handleEvent` method
if (handlerType === 'object' && (!fnOrObj.handleEvent || typeof fnOrObj.handleEvent !== 'function')) {
return;
}
const wrapperFn = getEventListenerWrapper(fnOrObj);
nativeWindowAddEventListener.call(this, type, wrapperFn as EventListener, optionsOrCapture);
}

function windowRemoveEventListener(this: EventTarget, type, fnOrObj, optionsOrCapture) {
const wrapperFn = getEventListenerWrapper(fnOrObj);
nativeWindowRemoveEventListener.call(this, type, wrapperFn || fnOrObj, optionsOrCapture);
}

function addEventListener(this: EventTarget, type, fnOrObj, optionsOrCapture) {
const handlerType = typeof fnOrObj;
// bail if `fnOrObj` is not a function, not an object
if (handlerType !== 'function' && handlerType !== 'object') {
return;
}
// bail if `fnOrObj` is an object without a `handleEvent` method
if (handlerType === 'object' && (!fnOrObj.handleEvent || typeof fnOrObj.handleEvent !== 'function')) {
return;
}
const wrapperFn = getEventListenerWrapper(fnOrObj);
nativeAddEventListener.call(this, type, wrapperFn as EventListener, optionsOrCapture);
}

function removeEventListener(this: EventTarget, type, fnOrObj, optionsOrCapture) {
const wrapperFn = getEventListenerWrapper(fnOrObj);
nativeRemoveEventListener.call(this, type, wrapperFn || fnOrObj, optionsOrCapture);
}

addEventListener.__lwcOriginal__ = nativeAddEventListener;
removeEventListener.__lwcOriginal__ = nativeRemoveEventListener;
windowAddEventListener.__lwcOriginal__ = nativeWindowAddEventListener;
windowRemoveEventListener.__lwcOriginal__ = nativeWindowRemoveEventListener;

function windowPatchListeners() {
window.addEventListener = windowAddEventListener;
window.removeEventListener = windowRemoveEventListener;
}

function nodePatchListeners() {
Node.prototype.addEventListener = addEventListener;
Node.prototype.removeEventListener = removeEventListener;
}

export default function apply() {
windowPatchListeners();
nodePatchListeners();
}

0 comments on commit 8d3fc9f

Please sign in to comment.