Skip to content

Commit

Permalink
adding docs, code, tests, tests working
Browse files Browse the repository at this point in the history
  • Loading branch information
orstavik committed Aug 5, 2020
1 parent d5477d0 commit e54dacb
Show file tree
Hide file tree
Showing 49 changed files with 6,702 additions and 3 deletions.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,24 @@ Polyfill for the getEventListeners method available in dev tools.

Includes a) extended event listener options such as 'first', 'last' and 'unstoppable' and b) a patch that will scope calls to 'stopPropagation()' to only apply to other event listeners under the same propagation root (ie. under the same ShadowRoot/window). These additions are separated in different modules.

hello sunshine on a [rainy](docs/rainy.md) day
1. [WhatIs EventTarget](1_WhatIs_EventTarget.md)
1. [WhatIs EquivalentEventListener](2_WhatIs_EquivalentEventListener.md)
1. [Why ExtendEventTarget](3_Why_ExtendEventTarget.md)
1. [Pattern EventTargetRegistry](4_Pattern_EventTargetRegistry.md)
1. [Pattern HasAndGetEventListeners](5_Pattern_HasAndGetEventListeners.md)
1. [WhatIs onclickEtc](5b_WhatIs_onclickEtc.md)
1. [Pattern OnceEventListenerOption](6_Pattern_OnceEventListenerOption.md)
1. [Pattern FirstEventListener](7_Pattern_FirstEventListener.md)
1. [Pattern EventListenerPriority](8_Pattern_EventListenerPriority.md)
1. [Pattern Event IsStopped](9_Pattern_Event_IsStopped.md)
1. [Pattern UnstoppableEventListeners](10_Pattern_UnstoppableEventListeners.md)
1. [Pattern LastEventListenerOption](12_Pattern_LastEventListenerOption.md)
1. [Pattern ScopedStopProp](13_Pattern_ScopedStopProp.md)
1. [Pattern GuaranteedBubbleListener](14_Pattern_GuaranteedBubbleListener.md)
1. [Pattern ImmediateOnlyEventListenerOption](15_Pattern_ImmediateOnlyEventListenerOption.md)

x. make a chapter about the getEventListeners() method in Chrome dev tools.
y. we need a discussion about the safety of the getEventListeners() method.
* functions can be retrieved from the dom elements. this means that there source can be printed. to avoid that, wrap them inside anoter function that calls them, or bind them.
* hide the access to the getEventListeners method, do not make it available via the window object for example, but keep its reference locked inside your framework js files.

224 changes: 224 additions & 0 deletions docs/10_Pattern_UnstoppableEventListeners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Pattern: UnstoppableEventListener

//todo this is old. The new ScopedStopPropagation replace this chapter.

The event listener option `{unstoppable: true}` ensures that event listeners added with this option will not be blocked when a previous event listener calls `stopPropagation()` or `stopImmediatePropagation()`.

## Why? When do we need `unstoppable` event listeners?

We need for `unstoppable` event listeners to avoid stopPropagationTorpedoes. The stopPropagation torpedoes is especially troublesome when you are adding event listeners for composed: true events inside the shadowDOM of a closed web component.

All web components must listen for composed: false events from within their shadowDOM. Obviously. But closed web component *must* also listen for the composed: true events from within their shadowDOM in order to know *which* element inside the shadowDOM that was targeted. For example, a `click` on one of two `<div>` elements inside a shadowDOM will need to read the inside target of the `click` event's propagation path, and this target is only accesible for event listeners added to a shadowDOM eventTarget.

`unstoppable` enables the event system to ensure that no accidental `stopPropagation()` in the `capture` phase blocks the event *before* it reaches the closed shadowRoot.

An alternative practice is to by convention do:
1. Never call `stopPropagation()` on events in the capture phase, unless you also intend and do call `preventDefault()`.
2. Never call `stopPropagation()` on an event inside a shadowRoot. This will cause strange behavior.
3. If default actions are to be added by web components, `stopPropagation()` anywhere will also mean `preventDefault()`.

## Implementation

To implement the `unstoppable` event listener option we need to override both the `Event.stopPropagation()` methods *and* the `EventTarget.addEventListener()` method. We do so by implementing a version of the `Event.isStopped` pattern that doesn't call the underlying, native `stopPropagation()` methods. We then wrap all event listener callback functions in a wrapper function that checks if the event has been stopped or not.

```javascript
//Event.isStopped and block the native stopPropagation() methods.
const isStoppedSymbol = Symbol("isStoppedSymbol");
Object.defineProperties(Event.prototype, {
"isStopped": {
get: function () {
return (this[isStoppedSymbol] && this[isStoppedSymbol] !== this.currentTarget) || false;
}
},
"stopPropagation": {
value: function () {
this[isStoppedSymbol] || (this[isStoppedSymbol] = this.currentTarget);
}
},
"stopImmediatePropagation": {
value: function () {
this[isStoppedSymbol] = true;
}
}
});
//Custom check of isStopped by wrapping all event listeners in a custom function that checks
//event.isStopped and options.unstoppable before running.
const cbToWrapperBubble = new WeakMap();//cache of wrapper functions (bubble listeners)
const cbToWrapperCapture = new WeakMap();//cache of wrapper functions (capture listeners)
const ogAdd = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function (type, cb, options) {
const cbToWrapper = (!options || (options instanceof Object) && !options.capture) ?
cbToWrapperBubble :
cbToWrapperCapture;
let wrapper = cbToWrapper.get(cb);
if (!wrapper) {
const unstoppable = options?.unstoppable;
wrapper = function (event) {
(!event.isStopped || unstoppable) && cb(event);
};
cbToWrapper.set(cb, wrapper);
}
ogAdd.call(this, type, wrapper, options);
}
});
```
Note: we need to ensure that the same wrapper function object is used for the same event listener callback, so that a new wrapper will not be created for the same object which would lead the underlying event listener system to possibly add multiple, duplicate event listeners. In addition, as the same listener function object will be added if their `capture` property differs, two `WeakMap()` caches must be used.
## Demo: unstoppable in the lightDOM
```html
<script>
(function () {

const isStoppedSymbol = Symbol("isStoppedSymbol");
Object.defineProperties(Event.prototype, {
"isStopped": {
get: function () {
return (this[isStoppedSymbol] && this[isStoppedSymbol] !== this.currentTarget) || false;
}
},
"stopPropagation": {
value: function () {
this[isStoppedSymbol] || (this[isStoppedSymbol] = this.currentTarget);
}
},
"stopImmediatePropagation": {
value: function () {
this[isStoppedSymbol] = true;
}
}
});

// overriding the stopPropagation logic by wrapping all functions in wrapper method
// the weakMaps preserves all wrappers for the same function objects, so that the event listener functions appear
// for the underlying event propagation system similarly as before.
const cbToWrapperBubble = new WeakMap();
const cbToWrapperCapture = new WeakMap();

const ogAdd = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function (type, cb, options) {
const cbToWrapper = (!options || (options instanceof Object) && !options.capture) ?
cbToWrapperBubble :
cbToWrapperCapture;
let wrapper = cbToWrapper.get(cb);
if (!wrapper) {
wrapper = function (event) {
(!event.isStopped || options?.unstoppable) && cb(event);
};
cbToWrapper.set(cb, wrapper);
}
ogAdd.call(this, type, wrapper, options);
}
});
})();
</script>

<div id="a">
<div id="b">
<div id="c">
hello sunshine
</div>
</div>
</div>

<script>
const a = document.querySelector("#a");
const b = document.querySelector("#b");
const c = document.querySelector("#c");

function aOnce() {
console.log("five, this should be only once, at the end")
}

a.addEventListener("click", e => console.log("one"), true);
a.addEventListener("click", e => e.stopPropagation(), true);
a.addEventListener("click", e => console.log("two"), true);
a.addEventListener("click", e => e.stopImmediatePropagation(), true);
a.addEventListener("click", aOnce, true);
b.addEventListener("click", e => console.log("three"), {unstoppable: true, capture: true});
c.addEventListener("click", e => console.log("---"), {capture: true});

c.addEventListener("click", e => console.log("---"));
b.addEventListener("click", e => console.log("four"), {unstoppable: true});
a.addEventListener("click", aOnce, {unstoppable: true});
</script>
```
## Demo: Unstoppable in the shadowDOM
```html
<script>
(function () {

const isStoppedSymbol = Symbol("isStoppedSymbol");
Object.defineProperties(Event.prototype, {
"isStopped": {
get: function () {
return (this[isStoppedSymbol] && this[isStoppedSymbol] !== this.currentTarget) || false;
}
},
"stopPropagation": {
value: function () {
this[isStoppedSymbol] || (this[isStoppedSymbol] = this.currentTarget);
}
},
"stopImmediatePropagation": {
value: function () {
this[isStoppedSymbol] = true;
}
}
});

// overriding the stopPropagation logic by wrapping all functions in wrapper method
// the weakMaps preserves all wrappers for the same function objects, so that the event listener functions appear
// for the underlying event propagation system similarly as before.
const cbToWrapperBubble = new WeakMap();
const cbToWrapperCapture = new WeakMap();

const ogAdd = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function (type, cb, options) {
const cbToWrapper = (!options || (options instanceof Object) && !options.capture) ?
cbToWrapperBubble :
cbToWrapperCapture;
let wrapper = cbToWrapper.get(cb);
if (!wrapper) {
wrapper = function (event) {
(!event.isStopped || options?.unstoppable) && cb(event);
};
cbToWrapper.set(cb, wrapper);
}
ogAdd.call(this, type, wrapper, options);
}
});
})();
</script>


<script>
class ClosedComp extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: "closed"});
shadow.innerHTML = `<div>Hello Sunshine</div>`;
shadow.children[0].addEventListener("click", e => console.log("unstoppable!!", e.composedPath()), {unstoppable: true});
}
}

customElements.define("closed-comp", ClosedComp);
</script>
<closed-comp></closed-comp>

<script>
window.addEventListener("click", e => console.log("click began propagation.", e.composedPath()), true);
window.addEventListener("click", e => e.stopPropagation(), true);
window.addEventListener("click", e => console.log("click has stopped propagation.", e.composedPath()), true);
//unstoppable!! will be written out still
</script>
```
## References
* [discussion about closed shadowDOM intention](https://github.com/w3c/webcomponents/issues/378#issuecomment-179596975)
111 changes: 111 additions & 0 deletions docs/12_Pattern_LastEventListenerOption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Pattern: `EventListenerOption: last`

`last` is an event listener option that ensures that the event listener is always called last on the current eventTarget in either the capture or the at-target phase. It is quite light and simple to implement.

## Solution

To ensure that an event listener runs last, we simply add two properties for each event name on each event target for either the bubble or the capture phase.

```javascript
//target => "eventName"/"eventName capture" => {cb, options}
const targetTypeLast = new WeakMap();

function getLast(target, type, cb, options){
const capture = options instanceof Object ? options.capture : !!options;
const lookupName = capture ? type + " capture" : type;
return targetTypeLast.get(target)?.get(lookupName);
}

function setLast(target, type, cb, options){
const capture = options instanceof Object ? options.capture : !!options;
const lookupName = capture ? type + " capture" : type;
let targetsMap = targetTypeLast.get(target);
if (!targetsMap)
targetTypeLast.set(target, targetsMap = new HashMap());
targetsMap.set(lookupName, {cb, options});
}

const original = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function(type, cb, options) {
const oldLast = getLast(this, type, options);
if (options?.last && oldLast)
throw new Error("only one last event listener can be added to a target for an event type at a time.");
if (options?.last){
setLast(this, type, cb, options);
return original.call(this, type, cb, options);
}
if (oldLast){
this.removeEventListener(type, oldLast.cb, oldLast.options);
const res = original.call(this, type, cb, options);
original.call(this, type, oldLast.cb, oldLast.options);
return res;
}
return original.call(this, type, cb, options);
}
});
```
## problem: `last: true` && `once: true`...
```javascript
//target => "eventName"/"eventName capture" => {cb, options}
const targetTypeLast = new WeakMap();

function getLast(target, type, cb, options){
const capture = options instanceof Object ? options.capture : !!options;
const lookupName = capture ? type + " capture" : type;
return targetTypeLast.get(target)?.get(lookupName);
}

function setLast(target, type, cb, options){
const capture = options instanceof Object ? options.capture : !!options;
const lookupName = capture ? type + " capture" : type;
let targetsMap = targetTypeLast.get(target);
if (!targetsMap)
targetTypeLast.set(target, targetsMap = new WeakMap());
if (options.once){ //once
const og = cb; //once
const me = this; //once
cb = function(e) { //once
me.removeEventListener(type, cb, options); //once
og.call(this, e); //once
}; //once
} //once
targetsMap.set(lookupName, {cb, options})
return cb; //once
}

const original = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function(type, cb, options) {
const oldLast = getLast(this, type, options);
if (options?.last && oldLast)
throw new Error("only one last event listener can be added to a target for an event type at a time.");
if (options?.last) {
cb = setLast(this, type, cb, options);
return original.call(this, type, cb, options);
}
if (oldLast){
this.removeEventListener(type, oldLast.cb, oldLast.options);
const res = original.call(this, type, cb, options);
original.call(this, type, oldLast.cb, oldLast.options);
return res;
}
return original.call(this, type, cb, options);
}
});

const original2 = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "removeEventListener", {
value: function(type, cb, options) {
const last = getLast(this, type, cb, options);
cb = last? last.cb : cb;
original2.call(this, type, cb, options);
}
});
```
## References
*

0 comments on commit e54dacb

Please sign in to comment.