Created
November 29, 2023 23:33
-
-
Save dfkaye/3affad4c43e363a84ac4320eae375129 to your computer and use it in GitHub Desktop.
onpropertychange signal v.7 -- method delegation in the proxy `get` handler
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
// 22 September 2023 | |
// onpropertychange signal v.7 | |
// cont'd from https://gist.github.com/dfkaye/1d9a55b8fe6659df7ba7c04edbd24bf7 | |
// Another one that seems to work (we get events from Array changes) but my | |
// reasoning in the comments seems hand-wavingly suspect. | |
// Method delegation adds more setup time (iterating and so on), another mapping | |
// dependency, and copying the prototype and setting that as the new prototype | |
// adds a (tiny) layer of indirection. | |
// But of course, the main issue, still is that Array.push() does not report any | |
// changes.... | |
//////////////////////////////////////////////////////////////////////////////// | |
// Experimental part up top. For the working solution in progress, scroll down | |
// to the "bridge" group... | |
//////////////////////////////////////////////////////////////////////////////// | |
var handler = { | |
get(target, key) { | |
// 22 September 2023 | |
// After struggling with the text problem of event-target.signal.js | |
// (see gist 14-21 August 2023 | |
// https://gist.github.com/dfkaye/9baf18152376dabc08664156e0106692), and the | |
// series of method call errors on elements, e.g., | |
// "TypeError: 'setAttribute' called on an object that does not implement | |
// interface Element.", or errors on arrays methods, e.g., | |
// "TypeError: s.push is not a function", where the bridge object must adopt | |
// them from the prototype because they are not enumerable on the instance, | |
// we have a solution. | |
// Method delegation allows updates to Text properties to synchronize without | |
// programming interventions, and calls to inherited methods such as push or | |
// setAttribute. | |
// Because we still need the EventTarget shim per control.target.js | |
// (see gist 4 August 2023 | |
// https://gist.github.com/dfkaye/f773b85fabf661108ed4ff7a2cdfc61d), to | |
// enable reactivity for any object that is not an instance of EventTarget, | |
// we (alas) need the bridge object. | |
var f = Reflect.get(target, key); | |
return typeof f == "function" | |
? function () { return f.apply(target, [...arguments]) } | |
: f; | |
}, | |
set(target, key, value) { | |
var oldValue = Reflect.get(target, key); | |
console.error({key, value, oldValue}); | |
if (key == 'onpropertychange') { | |
// remove add from node | |
console.warn("%csetting", "font-weight: bold;", key); | |
} | |
return Reflect.set(target, key, value); | |
} | |
}; | |
~(function () { | |
console.group("element"); | |
var input = document.createElement("input"); | |
var n = new Proxy(input, handler); | |
n.setAttribute("name", "value"); | |
n.addEventListener("click", function (e) { | |
console.warn("clicked"); | |
console.log(e.target); | |
}); | |
n.click(); | |
n.name = "named prop"; | |
console.log(JSON.stringify(n)); | |
console.groupEnd("element"); | |
})(); | |
~(function () { | |
console.group("array"); | |
var a = new Proxy(['a', 'b', 'c'], handler); | |
console.log(a.length); | |
a.push('d'); | |
console.log(a.length); | |
console.log(JSON.stringify(a)); | |
console.groupEnd("array"); | |
})(); | |
~(function () { | |
console.group("object"); | |
var o = new Proxy({ name: 'test'}, handler); | |
console.log(o); | |
o.name = "updated"; | |
console.log(o.toString()); | |
console.log(JSON.stringify(o)); | |
console.groupEnd("object"); | |
})(); | |
~(function () { | |
console.group("text"); | |
var t = new Proxy(new Text("text object"), handler); | |
t.textContent = "updated text"; | |
var {data, nodeValue, textContent, wholeText} = t; | |
console.log({data, nodeValue, textContent, wholeText}); | |
console.groupEnd("text"); | |
})(); | |
//////////////////////////////////////////////////////////////////////////////// | |
/* bridge group */ | |
//////////////////////////////////////////////////////////////////////////////// | |
~(function () { | |
// 22 September 2023 | |
// After struggling with the text problem of event-target.signal.js | |
// (see gist 14-21 August 2023 | |
// https://gist.github.com/dfkaye/9baf18152376dabc08664156e0106692), and the | |
// series of method call errors on elements, e.g., | |
// "TypeError: 'setAttribute' called on an object that does not implement | |
// interface Element.", or errors on arrays methods, e.g., | |
// "TypeError: s.push is not a function", where the bridge object must adopt | |
// them from the prototype because they are not enumerable on the instance, | |
// we have a solution. | |
// Use method delegation in the get() handler. | |
// Method delegation allows updates to Text properties to synchronize without | |
// programming interventions, and calls to inherited methods such as 'push' or | |
// 'setAttribute'. | |
// 23 September 2023 | |
// Because we still need the EventTarget shim per control.target.js | |
// (see gist 4 August 2023 | |
// https://gist.github.com/dfkaye/f773b85fabf661108ed4ff7a2cdfc61d), to | |
// enable reactivity for any object that is not an instance of EventTarget, | |
// we **don't** need a bridge object. Instead, we need to redefine the target | |
// as an EventTarget, and assign it any methods from the Array.prototype if | |
// it's an Array, and/or from the target itself. | |
// In the delegate map, we define a toString() method that returns the target | |
// in order to support JSON.stringify() and to override the default return | |
// "[object <Constructor>]" string pattern. | |
// And finally, for any methods in the target, we only create delegate methods | |
// for them we they are first requested by the get handler and store these in | |
// the delegate map for any subsequent access. | |
function map(t, f) { | |
return function () { return f.apply(t, [...arguments]); }; | |
} | |
function B(o) { | |
// TypeScript programmers are generally unaware of this but you can use the | |
// Object() constructor to insure that data always is an object. No need for | |
// arbitrary build steps, IDE hints or LSP/runtime crashes. | |
var target = Object(o); | |
// Store delegation methods here, creating any additional methods as we | |
// need them in the get() handler (no eager creation loop step needed). | |
var delegate = { | |
toJSON() { return target; }, | |
toString() { return target; } | |
}; | |
// Add reactivity to a target without an EventTarget interface. | |
// First, create an empty version of the target (Array or Object) to use as | |
// a new prototype for the target. This allows us to assign event methods to | |
// arrays and objects via this new prototype without polluting their actual | |
// prototypes or their 'own' property names, which in turn allows us to | |
// return the original target in the delegate.toString() method. | |
// Next, create a new EventTarget and map its methods as closures over the | |
// EventTarget on to the prototype. Because they are closures we can also | |
// assign them to the delegate map and avoid re-creating them... | |
// Then, set the prototype as the new prototype of the target. Since the | |
// target and the prototype are objects from the same constructor, the | |
// target still inherits the same properties, albeit in one additional step. | |
// Finally, assign these methods to the delegate map. | |
if (!(target instanceof EventTarget)) { | |
var prototype = Array.isArray(target) ? [] : {}; | |
var eventTarget = new EventTarget; | |
for (var name in eventTarget) { | |
prototype[name] = map(eventTarget, eventTarget[name]); | |
} | |
Object.setPrototypeOf(target, prototype); | |
Object.assign(delegate, prototype); | |
// Previous strategy was to create a new EventTarget and map properties | |
// from the target - and/or the target's prototype - to the EventTarget | |
// and assign the EventTarget as the target. That has the unfortunate | |
// consequences of discovering that dynamic Array properties are assigned | |
// to a non-Array object, and the target's constructor logs as an | |
// EventTarget instead of the original target's constructor. | |
/* | |
Array.isArray(target) | |
? ( | |
Object.getOwnPropertyNames(Array.prototype).forEach(function (k) { | |
p[k] = Array.prototype[k]; | |
}), | |
Object.getOwnPropertyNames(target).forEach(function (k) { | |
p[k] = target[k]; | |
}) | |
) | |
: Object.assign(p, target); | |
target = Object.assign(new EventTarget, p); | |
*/ | |
} | |
// Trap handler containing get(), set(), and even deleteProperty(). | |
var handler = { | |
get(target, key) { | |
if (typeof delegate[key] == "function") { | |
return delegate[key]; | |
} | |
var f = Reflect.get(target, key); | |
return typeof f == "function" | |
? delegate[key] = map(target, f) | |
: f; | |
}, | |
set(target, key, value) { | |
var previous = Reflect.get(target, key); | |
if (previous === value) { | |
return; | |
} | |
if (key == 'onpropertychange') { | |
console.warn("%csetting", "font-weight: bold;", key); | |
target.removeEventListener('propertychange', previous); | |
target.addEventListener('propertychange', value); | |
} | |
Reflect.set(target, key, value); | |
// 25 September 2023 | |
// delegates could get out of sync. nullify it here and let the next | |
// get() call re-map it. | |
if (typeof value == "function" && key in delegate) { | |
delegate[key] = null; | |
} | |
var event = new CustomEvent("propertychange", { | |
detail: { propertyName: key, value, previous } | |
}); | |
Object.defineProperty(event, 'target', { | |
value: target, | |
writable: true, | |
enumerable: true, | |
configurable: true | |
}); | |
return target.dispatchEvent(event); | |
}, | |
deleteProperty(target, key) { | |
var value = Reflect.get(target, key); | |
if (key === 'onpropertychange') { | |
target.removeEventListener('propertychange', value); | |
} | |
// 23 September 2023 | |
// Should warn or possibly use set(target, key, undefined)... | |
console.warn(`%cInstead of deleting ${key}, using`, "font-weight: bold;"); | |
console.log(`delete <object>.${key}`); | |
console.warn("%cconsider assignment, using", "font-weight: bold;"); | |
console.log(`<object>.${key} = undefined`); | |
Reflect.deleteProperty(target, key); | |
var event = new CustomEvent("propertychange", { | |
detail: { propertyName: key, deletedValue: value } | |
}); | |
return target.dispatchEvent(event); | |
} | |
}; | |
return new Proxy(target, handler); | |
} | |
console.group("bridge"); | |
console.group("input"); | |
var input = document.createElement('input'); | |
var t = B(input); | |
t.addEventListener("click", function (e) { | |
console.info("super click"); | |
console.log(e.detail.target, e.detail.test); | |
console.warn(e); | |
}); | |
t.dispatchEvent(new CustomEvent("click", { | |
detail: { target: t.toJSON(), test: "dispatchEvent" } | |
})); | |
t.click(); | |
t.setAttribute("name", "chosen"); | |
console.log(t.outerHTML); | |
console.warn(t.toString()); | |
console.log(JSON.stringify(t)); | |
t.name = "this is the name"; | |
t.value = "this is the value"; | |
t.data = "nonstantine"; | |
console.warn(t.toString()); | |
// name and value don't serialize - they are part of the prototype chain, and | |
// and are defined with getters, making them non-enumerable, and hence, non- | |
// serializable. | |
// data on the other hand is an expando or dynamic property, so it serializes. | |
console.log(JSON.stringify(t, null, 2)); | |
console.groupEnd("input"); | |
console.group("text"); | |
var o = new Text("text node") | |
var t = B(o); | |
t.data = "* data updated *" | |
t.addEventListener("click", function (e) { | |
console.info("super click"); | |
console.log(e); | |
var {data, nodeValue, textContent, wholeText} = e.target; | |
console.log({data, nodeValue, textContent, wholeText}); | |
}); | |
t.dispatchEvent(new CustomEvent("click", { | |
detail: { test: "dispatchEvent" } | |
})); | |
console.groupEnd("text"); | |
console.group("array"); | |
var a = B(['a', 'b', 'c']); | |
// mutating calls | |
console.log(a.toString()); | |
console.warn(a.push('d')); | |
console.log(a.toString()); | |
a[a.length] = 'e'; | |
console.log(a.toString()); | |
console.error(a); | |
//console.log( a.reverse() ); | |
console.log( a[0] ); | |
a.addEventListener("click", function (e) { | |
console.info("Array click"); | |
console.warn(e); | |
}); | |
// click event on array works... | |
a.dispatchEvent(new CustomEvent("click", { | |
detail: { test: "dispatchEvent on an array" } | |
})); | |
// ... but push mutations are still not reported. | |
a.push('1000', '1001'); | |
console.groupEnd("array"); | |
console.group("onpropertychange"); | |
var p = B({ test: "onpropertychange"}); | |
function h(e) { | |
console.info("propertychange handler"); | |
console.log(e.target); | |
console.log(e.detail); | |
} | |
p.addEventListener("propertychange", h); | |
console.assert(!p.onpropertychange); | |
p.onpropertychange = function m(e) { | |
console.info("onpropertychange method"); | |
console.log(e.target); | |
console.log(e.detail); | |
}; | |
p.test = "updated"; | |
p.next = 'next'; | |
console.warn(p.toString()); | |
console.log(JSON.stringify(p)); | |
delete p.test; | |
delete p.onpropertychange; | |
delete p.next; | |
console.warn(p.toString()); | |
console.log(JSON.stringify(p)); | |
console.groupEnd("onpropertychange"); | |
console.group("delegate"); | |
var p = B({ test: "delegate"}); | |
p.onpropertychange = function m(e) { | |
console.info("delegate test"); | |
console.log(e.target, e.detail); | |
}; | |
p.test = function (e) { | |
console.log("test"); | |
console.log(e); | |
}; | |
p.test({ data: "test delegate" }); | |
p.test = function test2(e) { | |
console.log("test2"); | |
console.log(e); | |
}; | |
p.test({ data: "test2 delegate" }); | |
console.groupEnd("delegate"); | |
console.group("event tricks"); | |
var p = B({ test: "event tricks"}); | |
p.addEventListener("test", function (e) { | |
console.warn( e.target, e.detail ); | |
console.log( e ); | |
}); | |
p.prop = "another one"; | |
var event = new CustomEvent("test", { | |
detail: { value: 1 }, | |
}); | |
// Override event properties with Object.defineProperty(), including the | |
// target. | |
// https://gist.github.com/dfkaye/68b9bfe29cb85cf1145574c1c62fd859#overriding-event-properties | |
Object.defineProperty(event, 'target', { | |
value: p.toJSON(), | |
writable: true, | |
enumerable: true, | |
configurable: true | |
}); | |
p.dispatchEvent(event); | |
console.groupEnd("event tricks"); | |
console.groupEnd("bridge"); | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment