Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Created November 29, 2023 23:12
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/d849596bdb9e926ad69ad38c974a4763 to your computer and use it in GitHub Desktop.
Save dfkaye/d849596bdb9e926ad69ad38c974a4763 to your computer and use it in GitHub Desktop.
onpropertychange signal v.2 - implementation with proxied objects
// 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