Last active
April 14, 2024 06:30
-
-
Save dfkaye/2c7de4ba9bf7758f3c052378ce46219a to your computer and use it in GitHub Desktop.
onpropertychange signal v.9 -- Success! v.9 supports both Object and EventTarget
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 27 September 2023 | |
// onpropertychange signal v.9 | |
// cf. v.8 https://gist.github.com/dfkaye/548622151971122110bf4047ae3ac432 | |
// and v.1 https://gist.github.com/dfkaye/619c5f31080fce2cd383ac966e132311 | |
// Success, at last! v.9 supports both Object and EventTarget. | |
// Investigating where proxy on an array allows array.push mutations, the goal | |
// here was to get the proxy to work on EventTarget and Object, this time with | |
// separate enhancement steps. We define a separate get() handler and delegate | |
// for EventTargets (so 'setAttribute' doesn't fail), while polyfilling event | |
// methods on non-EventTargets (so 'addEventListener' doesn't fail). | |
// All passing for input, Text, object, and Array, including input.setAttribute, | |
// text.data synchronizing with textContent, wholeText, and nodeValue, and | |
// array.push, .splice, .pop, .sort, reverse... | |
// 2 October 2023 | |
// Added test groups for event: handleEvent, errors and properties. | |
// Interesting things: | |
// Array mutation methods make the reporting noisy as *every* change is fired. | |
// See the splice test in particular where the result of each reordering step is | |
// reported in the console. | |
// Appears that multiple removed event listeners still fire on arrays and | |
// objects, but that is an illusion due to the event errors test which sets the | |
// onpropertychange handler three 3 times with functions that throw errors, then | |
// an error-free function. The illusion comes from the act of setting and then | |
// calling the new handler for this property as part of dispatching events. | |
// Discovered that `target` for object/array propertychange events must be set | |
// or overridden using defineProperty, otherwise target returns as null for | |
// arrays. | |
// Still need to decide on the event shape (Event vs. CustomEvent or extends the | |
// Event constructor as PropertyChangeEvent or uses detail for propertyName, | |
// value, oldValue or defines these as own properties on the event...). | |
// 1 December 2023 | |
// added Date object support in the get handler, e.g., | |
// code: `new Proxy(new Date, {}).getTime();`` | |
// message: "TypeError: Date.prototype.getTime called on incompatible Proxy" | |
function E(o) { | |
var target = Object(o); | |
if (target instanceof EventTarget) { | |
var delegate = {}; | |
function m(t, k) { | |
return function () { | |
return t[k](...arguments); | |
} | |
} | |
// Delegate EventTarget methods that handler.get() will use, so we don't get | |
// Uncaught TypeError: 'setAttribute' called on an object that does not | |
// implement interface Element. | |
for (var k in target) { | |
if (typeof target[k] == "function") { | |
delegate[k] = m(target, k); | |
} | |
} | |
var handler = { | |
get(target, key) { | |
if (typeof delegate[key] == "function") { | |
return delegate[key]; | |
} | |
return Reflect.get(target, key); | |
}, | |
set(target, key, value) { | |
var previous = Reflect.get(target, key); | |
if (previous === value) { | |
return true; | |
} | |
if (key == "onpropertychange") { | |
target.removeEventListener("propertychange", previous); | |
target.addEventListener("propertychange", value); | |
if (value == null) { | |
return Reflect.deleteProperty(target, key); | |
} | |
} | |
if (key in delegate) { | |
// If our programmer overrides this key on the target, stop tracking | |
// it in the delegate list. Better to let the new behavior fail than | |
// to prohibit the flexibility to override the environment. With great | |
// power comes great responsibility. | |
delegate[key] = null; | |
} | |
Reflect.set(target, key, value); | |
var event = new CustomEvent("propertychange", { | |
detail: { key, value, previous } | |
}); | |
return target.dispatchEvent(event); | |
} | |
}; | |
return new Proxy(target, handler); | |
} | |
// Not an instance of EventTarget: | |
// Add event listeners to non-event-targets so we don't get the error, | |
// "Uncaught TypeError: o.addEventListener is not a function" | |
var listeners = new Map; | |
Object.defineProperties(target, { | |
"addEventListener": { | |
value: function addEventListener(type, h) { | |
h = Object(h); | |
if (!(typeof h == "function" || typeof h.handleEvent == "function")) { | |
return false; | |
} | |
if (!listeners.has(type)) { | |
listeners.set(type, new Set); | |
} | |
return listeners.get(type).add(h); | |
} | |
}, | |
"removeEventListener": { | |
value: function removeEventListener(type, h) { | |
if (!listeners.has(type)) { | |
return false; | |
}; | |
return listeners.get(type).delete(h); | |
} | |
}, | |
"dispatchEvent": { | |
value: function dispatchEvent(event) { | |
var { type } = event; | |
if (!listeners.has(type)) { | |
return false; | |
}; | |
var errors = []; | |
listeners.get(type).forEach(function (h) { | |
var f = typeof h == "function" | |
? h | |
: typeof h.handleEvent == "function" | |
? h.handleEvent | |
: null; | |
// Without this assignment, target is null on arrays (!) | |
Object.defineProperty(event, 'target', { | |
value: target, enumerable: true | |
}); | |
try { var error; f.call(target, event); } | |
catch (e) { error = e; console.error(error); errors.push(error);} | |
}); | |
return !errors.length ? true : { errors }; | |
} | |
} | |
}); | |
var handler = { | |
get(target, key) { | |
// 1 December 2023 | |
// use bind approach to avoid TypeError - incompatible proxy message for Dates. | |
var v = Reflect.get(target, key); | |
return typeof v == 'function' && target instanceof Date | |
? v.bind(target) | |
: v; | |
}, | |
set(target, key, value) { | |
var previous = Reflect.get(target, key, value); | |
if (previous === value) { | |
return true; | |
} | |
if (key == "onpropertychange") { | |
target.removeEventListener("propertychange", previous); | |
target.addEventListener("propertychange", value); | |
if (value == null) { | |
return Reflect.deleteProperty(target, key); | |
} | |
// Should we report a new propertychange to onpropertychange? | |
//return Reflect.set(target, key, value); | |
} | |
Reflect.set(target, key, value); | |
var event = new Event("propertychange", { | |
detail: { propertyName: key, value, oldValue: previous } | |
}); | |
return target.dispatchEvent(event); | |
} | |
}; | |
return new Proxy(target, handler); | |
} | |
/* test it out */ | |
console.group("setup"); | |
function h(e) { | |
console.info("propertychange listener"); | |
console.warn(e); | |
} | |
function f(e) { | |
console.info("onpropertychange function"); | |
console.warn(e); | |
} | |
function g(e) { | |
console.info('g'); | |
console.log(e); | |
} | |
console.groupEnd("setup"); | |
console.group("element"); | |
var e = E(document.createElement('input')); | |
console.log( e.outerHTML ); | |
e.addEventListener('propertychange', h); | |
e.onpropertychange = f; | |
e.setAttribute('name', "renamed"); | |
e.name = 'f'; | |
console.log( e.outerHTML ); | |
e.onpropertychange = g; | |
e.onpropertychange = null; | |
e.name = 'expect only listener'; | |
console.group("override a delegate method"); | |
e.setAttribute = function () { | |
var [name, value] = arguments; | |
console.log("calling setAttribute:", { name, value}); | |
// 28 September 2023 | |
// This is how to create your own DOM attribute and apply it to a node's | |
// attributes (NamedNodeMap). | |
var attr = document.createAttribute(name); | |
attr.value = value; | |
this.attributes.setNamedItem(attr); | |
console.log(this.attributes); | |
} | |
e.setAttribute("id", "ID"); | |
e.setAttribute("id", "updatedID"); | |
// querySelector | |
e.appendChild(Object.assign(document.createElement("a"), { | |
className: "inner", | |
href: "//_._/_?abc=def", | |
textContent: "exciting inner anchor element" | |
})); | |
console.error( e.querySelector('a').outerHTML ); | |
// <a class="inner" href="//_._/_?abc=def">exciting inner anchor element</a> | |
console.groupEnd("override a delegate method"); | |
console.groupEnd("element"); | |
console.group("text"); | |
var t = E(new Text("here's a text node")); | |
console.log( t.data ); | |
t.addEventListener('propertychange', h); | |
t.onpropertychange = f; | |
t.wholeText = "wholeText"; | |
t.textContent = "textContent"; | |
t.onpropertychange = g; | |
t.onpropertychange = null; | |
t.name = 'expect only listener'; | |
console.groupEnd("text"); | |
console.group("object"); | |
var o = E({test: "** object **"}); | |
console.log( o.test ); | |
o.addEventListener('propertychange', h); | |
o.onpropertychange = f; | |
o.test = "modified text"; | |
o.test = "rapidly updated text"; | |
o.onpropertychange = g; | |
o.onpropertychange = null; | |
o.name = 'expect only listener'; | |
o.removeEventListener('propertychange', h); | |
o.addEventListener('propertychange', function (e) { | |
console.log( JSON.stringify(e.target, null, 2) ); | |
}); | |
o.name = "wham"; | |
console.groupEnd("object"); | |
console.group("array"); | |
var a = E(['a', 'b', 'c']); | |
console.log( a ); | |
a.onpropertychange = function (e) { | |
console.warn( e ); | |
console.log( JSON.stringify(e.target, null, 2) ); | |
}; | |
a[0] = "modified text"; | |
a[0] = "rapidly updated text"; | |
a.push('expect only listener'); | |
a.push("wham"); | |
a.splice(1, 3, "inserted"); | |
a.push("one more"); | |
a.pop(); | |
a.sort(); | |
a.reverse(); | |
console.groupEnd("array"); | |
console.group("events"); | |
console.group("handleEvent"); | |
var a = E({}); | |
a.addEventListener("propertychange", { | |
handleEvent(e) { console.log("object handleEvent:", e); } | |
}); | |
a[0] = 'd'; | |
var e = E(document.createElement("input")); | |
e.setAttribute("name", "events"); | |
e.addEventListener("propertychange", { | |
handleEvent(e) { console.warn("input handleEvent:", e); } | |
}); | |
e[0] = 'd'; | |
console.groupEnd("handleEvent"); | |
console.group("error"); | |
var a = E({}); | |
a.onpropertychange = function (e) { | |
throw new TypeError("types won't save you"); | |
}; | |
a.onpropertychange = function (e) { | |
throw new TypeError("the IDE won't save you"); | |
}; | |
a.onpropertychange = function (e) { | |
throw new TypeError("compilers won't save you"); | |
}; | |
a.onpropertychange = function (e) { | |
console.log("OK", e); | |
}; | |
a.addEventListener("propertychange", function (e) { | |
throw new TypeError("A"); | |
}); | |
a.addEventListener("propertychange", function (e) { | |
console.log("A"); | |
}); | |
a.addEventListener("propertychange", function (e) { | |
throw new TypeError("B"); | |
}); | |
a.addEventListener("propertychange", function (e) { | |
console.log("B"); | |
}); | |
a[0] = 'd'; | |
a[1] = 'e'; | |
a[2] = 'f'; | |
var result = a.dispatchEvent(new CustomEvent("propertychange", { | |
detail: { value: "testing errors" } | |
})); | |
console.warn( result.errors.length === 2, "should be 2 errors" ); | |
console.groupEnd("errors"); | |
console.group("properties"); | |
var a = E(['a']); | |
a.onpropertychange = function (e) { | |
console.log( Object.hasOwn(e.target, "1") ); | |
}; | |
a[1] = 'd'; | |
console.groupEnd("properties"); | |
console.groupEnd("event"); | |
console.group("date"); | |
console.group("object"); | |
var dateObject = E(new Date); | |
console.log( dateObject.getTime() ); | |
console.groupEnd("object"); | |
console.group("string"); | |
var dateString = E(Date()); | |
console.log( dateString ); | |
console.groupEnd("string"); | |
console.groupEnd("date"); | |
console.group("array splice"); | |
// 13 April 2024 | |
var a = E(['a', 'b', 'c']); | |
a.onpropertychange = function (e) { | |
console.log( JSON.stringify(e.target, null, 2) ); | |
}; | |
console.group("shift"); | |
// logs 3 times | |
/* | |
[ | |
"b", | |
"b", | |
"c" | |
] | |
[ | |
"b", | |
"c", | |
"c" | |
] | |
[ | |
"b", | |
"c" | |
] | |
*/ | |
a.splice(0, 1); | |
console.groupEnd("shift"); | |
console.group("push"); | |
// logs 1 time | |
/* | |
[ | |
"b", | |
"c", | |
"appended item" | |
] | |
*/ | |
a.splice(a.length, 1, 'appended item'); | |
console.groupEnd("push"); | |
console.log(a); | |
console.groupEnd("array splice"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment