Created
November 29, 2023 23:12
-
-
Save dfkaye/d849596bdb9e926ad69ad38c974a4763 to your computer and use it in GitHub Desktop.
onpropertychange signal v.2 - implementation with proxied objects
This file contains hidden or 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
// 16 september 2023 | |
// onpropertychange signal v.2 | |
// An implementation with proxied objects. | |
// cont'd from https://gist.github.com/dfkaye/619c5f31080fce2cd383ac966e132311 | |
//////////////////////////////////////////////////////////////////////////////// | |
// early afternoon | |
// var target = []; | |
// var handler = { | |
// defineProperty(target, key, descriptor) { | |
// var previous = target[key]; | |
// var value = descriptor.value; | |
// console.log(previous, value); | |
// if (previous === value) { | |
// return true; | |
// } | |
// console.warn(target, key, descriptor); | |
// Reflect.set(target, key, value); | |
// var event = new CustomEvent("propertychange", { | |
// detail: { previous, value } | |
// }); | |
// target.dispatchEvent(event); | |
// return true; | |
// }, | |
// deleteProperty() { | |
// console.log("deleted:", ...arguments); | |
// } | |
// }; | |
// var p = new Proxy(target, handler); | |
// p.push(...[1,2,3]); | |
// /* | |
// var input = document.createElement("input"); | |
// var i = new Proxy(input, handler); | |
// i.setAttribute('fake', 'value') | |
// // Uncaught TypeError: 'setAttribute' called on an object that does not | |
// // implement interface Element. | |
// */ | |
// /* | |
// target = {}; | |
// p = new Proxy(target, handler); | |
// p.name = "test"; | |
// target | |
// */ | |
// target = new EventTarget; | |
// target.addEventListener("propertychange", function (e) { | |
// console.log(e.detail); | |
// }); | |
// p = new Proxy(target, handler); | |
// p.name = 'events'; | |
// // suppose we hide the target internally, our proxy interface requires a | |
// // method to add listeners. that method cannot be named the same as the | |
// // target method because of internal recursion... | |
// p.addListener = function (name, fn) { | |
// return target.addEventListener(name, fn) | |
// }; | |
// p.addListener("propertychange", function (e) { | |
// console.warn(e.detail); | |
// }); | |
// p.name = 'tusk'; | |
//////////////////////////////////////////////////////////////////////////////// | |
// 16 september 2023 | |
// afternoon | |
// class extends EventTarget | |
// this could work but the proxy must still define the same event listener | |
// methods independent of the base class... | |
// class O extends EventTarget {} | |
// target = new O | |
// // yes | |
// target.addEventListener("propertychange", function (e) { | |
// console.log(e); | |
// }); | |
// p = new Proxy(target, handler); | |
// // No | |
// p.addEventListener("propertychange", function (e) { | |
// console.log(e); | |
// }); | |
// // Uncaught TypeError: 'addEventListener' called on an object that does not | |
// // implement interface EventTarget. | |
// p.name = 'events'; | |
//////////////////////////////////////////////////////////////////////////////// | |
// 16 September 2023 | |
// afternoon: | |
// so target should be an object or data | |
// proxy should create an event target internally | |
// and define passthrough or event method facades on the data... | |
// | |
// evening: | |
// ...or proxy for an interface with methods that forward to the internal event | |
// target... | |
// ...and the traps read and write the target for previous and next value, for | |
// deleting values, and even supporting the onpropertychange DOM Event 0 method. | |
// 17 September 2023 | |
// reorganized linear console assertions into groups | |
// fixed api.removeEventListener propertychange and onpropertychange sync'ing | |
// 18 September 2023 | |
// Attempt to support array data - unsuccessful, way too complicated, at least | |
// for this implementation... | |
// Did catch setup bugs so that's nice. | |
// However, array.push fails (18 September: due to setting the data's prototype, | |
// which replaces the Array.prototype methods entirely...) | |
function P(data) { | |
data = Object(data); | |
var target = Object.assign(new EventTarget, data); | |
var API = { | |
addEventListener(name, h) { target.addEventListener(name, h); }, | |
removeEventListener(name, h) { | |
if (name == "propertychange" && h === this.onpropertychange) { | |
return delete this.onpropertychange; | |
} | |
return target.removeEventListener(name, h); | |
}, | |
dispatchEvent(event) { target.dispatchEvent(event); }, | |
toJSON() { return target; }, | |
toString() { return JSON.stringify(target); }, | |
onpropertychange: null, | |
api() { return Object.getPrototypeOf(this); } | |
}; | |
Object.setPrototypeOf(data, API); | |
var handler = { | |
defineProperty(data, key, descriptor) { | |
var value = descriptor.value; | |
if (key == 'onpropertychange') { | |
var h = Reflect.get(data, key); | |
target.removeEventListener('propertychange', h); | |
target.addEventListener('propertychange', value); | |
return Reflect.set(data, key, value); | |
} | |
var previous = Reflect.get(target, key); | |
if (previous === value) { | |
return; | |
} | |
Reflect.set(target, key, value); | |
var event = new CustomEvent("propertychange", { | |
detail: { propertyName: key, previous, value } | |
}); | |
return target.dispatchEvent(event); | |
}, | |
deleteProperty(data, key) { | |
if (key == 'onpropertychange') { | |
var h = Reflect.get(data, key); | |
Reflect.deleteProperty(data, key); | |
return target.removeEventListener('propertychange', h); | |
} | |
return Reflect.deleteProperty(target, key); | |
} | |
}; | |
return new Proxy(data, handler); | |
} | |
/* test it out */ | |
console.group("propertychange event"); | |
(function () { | |
var p = P({ name: "propertychange event" }); | |
function f(e) { | |
console.assert( | |
e.detail.propertyName == "name", | |
`should return propertyName, "name"` | |
); | |
console.assert( | |
e.detail.previous == "propertychange event", | |
`should return previous, "propertychange event"` | |
); | |
console.assert( | |
e.detail.value == "UPDATED", | |
`should return detail.value, "UPDATED"` | |
); | |
console.assert( | |
e.type == "propertychange", | |
`should return type, "propertychange"` | |
); | |
console.assert( | |
e.target.name == "UPDATED", | |
`should set target.name, "UPDATED"` | |
); | |
}; | |
p.addEventListener("propertychange", f); | |
p.name = "UPDATED"; | |
p.removeEventListener("propertychange", f); | |
p.name = "SHOULD NOT DISPATCH EVENT"; | |
})(); | |
console.groupEnd("propertychange event"); | |
console.group("dispatch customEvent"); | |
(function () { | |
var p = P({ name: "dispatch customEvent" }); | |
function f(e) { | |
console.assert( | |
!("propertyName" in e.detail), | |
`should not return "propertyName"` | |
); | |
console.assert( | |
!("previous" in e.detail), | |
`should not return "previous"` | |
); | |
console.assert( | |
e.detail.value == "DISHES", | |
`should return detail.value, "DISHES"` | |
); | |
console.assert( | |
e.type == "chore", | |
`should return type, "chore"` | |
); | |
console.assert( | |
e.target.name == "dispatch customEvent", | |
`should return target.name, "dispatch customEvent"` | |
); | |
} | |
p.addEventListener("chore", f); | |
p.dispatchEvent(new CustomEvent("chore", { detail: { value: "DISHES" } })); | |
p.removeEventListener("chore", f); | |
p.dispatchEvent(new CustomEvent("chore", { | |
detail: { value: "SHOULD NOT SEE THIS" } | |
})); | |
})(); | |
console.groupEnd("dispatch customEvent"); | |
console.group("onpropertychange handler"); | |
(function () { | |
var p = P({ name: "onpropertychange handler" }); | |
function f(e) { | |
console.assert( | |
e.detail.previous == "onpropertychange handler", | |
`should return previous, "onpropertychange handler"` | |
); | |
console.assert( | |
e.detail.value == "CHANGED", | |
`should return detail.value, "CHANGED"` | |
); | |
console.assert( | |
e.type == "propertychange", | |
`should return type, "propertychange"` | |
); | |
console.assert( | |
e.target.name == "CHANGED", | |
`should return target.name, "CHANGED"` | |
); | |
} | |
p.onpropertychange = f; | |
p.name = 'CHANGED'; | |
p.onpropertychange = null; | |
p.name = "onpropertychange handler"; | |
p.onpropertychange = f; | |
p.name = 'CHANGED'; | |
p.removeEventListener("propertychange", f); | |
console.assert( | |
p.onpropertychange === null, | |
"removing handler should set onpropertychange to null" | |
); | |
p.name = "REMOVED"; | |
delete p.onpropertychange; | |
console.assert( | |
p.onpropertychange === null, | |
"deleting handler should set onpropertychange to null" | |
); | |
})(); | |
console.groupEnd("onpropertychange handler"); | |
console.group("added once"); | |
(function () { | |
var p = P({ name: "added once" }); | |
var i = 0; | |
function f(e) { | |
i += 1; | |
console.assert( | |
i === 1, | |
`should call handler only once, not "${i}" times` | |
); | |
console.assert( | |
e.detail.previous == "added once", | |
`should return previous, "added once"` | |
); | |
console.assert( | |
e.detail.value == "CHANGED", | |
`should return detail.value, "CHANGED"` | |
); | |
console.assert( | |
e.type == "propertychange", | |
`should return type, "propertychange"` | |
); | |
console.assert( | |
e.target.name == "CHANGED", `should return target.name, | |
"CHANGED"` | |
); | |
} | |
p.addEventListener("propertychange", f); | |
p.addEventListener("propertychange", f); | |
p.onpropertychange = f; | |
p.name = "CHANGED"; | |
})(); | |
console.groupEnd("added once"); | |
console.group("event order"); | |
(function () { | |
var p = P({ name: "event order" }); | |
var i = 0; | |
function a(e) { | |
i += 1; | |
console.assert(i == 1, "should be called 1st") | |
console.assert(e.target.name == "CHANGED", `should show name, "CHANGED"`); | |
} | |
function b(e) { | |
"use strict"; | |
i += 1; | |
console.assert(i == 2, "should be called 2nd"); | |
try { var message; throw Error(e.target.name); } | |
catch (error) { message = error.message; } | |
finally { | |
console.assert( | |
message == "CHANGED", | |
`should see error message, "CHANGED"` | |
); | |
} | |
} | |
function c(e) { | |
i += 1; | |
console.assert(i == 3, "should be called 3rd"); | |
console.assert(e.target.name == "CHANGED", `should handle name, "CHANGED"`); | |
} | |
function d(e) { | |
i += 1; | |
console.assert(i == 4, "should be called 4th") | |
console.assert(e.target.name == "CHANGED", `should show name, "CHANGED"`); | |
} | |
p.addEventListener("propertychange", a); | |
p.addEventListener("propertychange", b); | |
p.onpropertychange = c; | |
p.addEventListener("propertychange", d); | |
p.name = "CHANGED"; | |
})(); | |
console.groupEnd("event order"); | |
console.group("serialize"); | |
(function () { | |
var p = P({ name: "serialize" }); | |
console.assert( | |
JSON.stringify(p) === `{"name":"serialize"}`, | |
`should serialize to {"name":"serialize"}` | |
); | |
console.assert( | |
p.toJSON().name === "serialize", | |
`toJSON() should return event target` | |
); | |
console.assert( | |
p.toString() === JSON.stringify(p), | |
`should stringify to JSON` | |
); | |
console.assert(p.valueOf() === p, 'should return self'); | |
})(); | |
console.groupEnd("serialize"); | |
console.group("API"); | |
(function () { | |
var p = P({ name: "API" }); | |
var api = p.api(); | |
Reflect.ownKeys(api).forEach(function (key) { | |
console.assert(api[key] === p[key], `should proxy key, "${key}"`); | |
if (key == "onpropertychange") { | |
console.assert( | |
api.onpropertychange === null, | |
`onpropertychange should be null` | |
); | |
} | |
else { | |
console.assert( | |
typeof api[key] == "function", | |
`should have method, "${key}"` | |
); | |
} | |
}); | |
})(); | |
console.groupEnd("API"); | |
console.group("arrays"); | |
(function () { | |
// 18 September 2023 | |
// more work to do on here.... | |
// Array methods appear not to be accessible to the Proxy | |
var p = P([ ]); | |
var target; | |
p.onpropertychange = function (e) { | |
target || (target = e.target); | |
console.warn(e); | |
}; | |
p[0] = 1; | |
// Array.push() fails.... | |
try { | |
var message; | |
p.push(2); | |
} | |
catch (e) { | |
message = e.message; | |
console.error(message); | |
// p.push is not a function | |
} | |
finally { | |
console.assert( | |
message.includes("push is not a function"), | |
"should get an error" | |
); | |
} | |
p.length = 0; | |
console.log(target["0"], target.length); | |
console.log(p[0], p.length); | |
console.assert( | |
target["0"] === 1 && target.length === 0, | |
"target sync'ing is hard" | |
); | |
console.assert( | |
p[0] === undefined && p.length === 0, | |
"proxy sync'ing is hard" | |
); | |
})(); | |
console.groupEnd("arrays"); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment