Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active April 14, 2024 06:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfkaye/2c7de4ba9bf7758f3c052378ce46219a to your computer and use it in GitHub Desktop.
Save dfkaye/2c7de4ba9bf7758f3c052378ce46219a to your computer and use it in GitHub Desktop.
onpropertychange signal v.9 -- Success! v.9 supports both Object and EventTarget
// 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