Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Created November 29, 2023 23:33
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/3affad4c43e363a84ac4320eae375129 to your computer and use it in GitHub Desktop.
Save dfkaye/3affad4c43e363a84ac4320eae375129 to your computer and use it in GitHub Desktop.
onpropertychange signal v.7 -- method delegation in the proxy `get` handler
// 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