Last active
November 29, 2023 23:50
-
-
Save dfkaye/619c5f31080fce2cd383ac966e132311 to your computer and use it in GitHub Desktop.
onpropertychange signal v.1 - using object descriptors
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
// 15 September 2023 | |
// onpropertychange signal v.1 | |
// This begins a 12-day odyssey in search of a successful implementation of the | |
// `onpropertychange` handler once supported by Internet Explorer in browser | |
// days of yore. | |
// cf. v.9 for the successful solution that solves both Object and EvenTarget, | |
// https://gist.github.com/dfkaye/2c7de4ba9bf7758f3c052378ce46219a | |
// Define Object.prototype.onpropertychange | |
// Very limited implementation because we have to register each property name, | |
// individually. Also the prototype methods, such as array.push() and what-have- | |
// you, don't trigger the property access. | |
// TODO: | |
// Maybe we should support EventTarget... | |
// Should test behavior differences between form element attributes and | |
// properties... | |
// Use this to watch property name access. | |
var map = new Map; | |
// Use prototype enhancement/pollution so every object gets one. | |
Object.prototype.onpropertychange = function(propertyName, handler) { | |
handler = Object(handler); | |
var fn, context; | |
if (typeof handler == "function") { | |
fn = handler; | |
context = this; | |
} | |
else if (typeof handler.handleEvent == "function") { | |
fn = handler.handleEvent; | |
context = handler; | |
} | |
else { | |
return false; | |
} | |
if (!map.has(this)) { | |
map.set(this, new Map); | |
} | |
var properties = map.get(this); | |
properties.set(propertyName, this[propertyName]); | |
Object.defineProperty(this, propertyName, { | |
get() { | |
return properties.get(propertyName) | |
}, | |
set(newValue) { | |
var previous = properties.get(propertyName); | |
if (previous === newValue) { | |
console.log(`should not set "${newValue}"`); | |
return false; | |
} | |
properties.set(propertyName, newValue); | |
var event = new CustomEvent("propertychange", { | |
detail: { previous, newValue } | |
}); | |
fn.call(context, event); | |
return newValue; | |
} | |
}); | |
return true; | |
}; | |
// We need a way to turn off the access watching... | |
Object.prototype.stop = function (propertyName) { | |
if (!map.has(this)) { | |
return false; | |
} | |
var properties = map.get(this); | |
var value = properties.get(propertyName); | |
// Remove this property from the observed name list. | |
properties.delete(propertyName); | |
// Remove the current property definition to stop observing changes. | |
delete this[propertyName]; | |
// Re-set this property value to the observed value. | |
this[propertyName] = value; | |
return true; | |
} | |
/* test it out */ | |
var a = { name: "test" }; | |
console.log( | |
a.onpropertychange("name", function (event) { | |
console.warn(this); | |
console.log(event.detail); | |
}) | |
); | |
// true | |
a.onpropertychange("name", { | |
handleEvent: function (event) { | |
console.warn(this); | |
console.log(event.detail); | |
}, | |
name: "handleEvent test" | |
}); | |
// Object { handleEvent: handleEvent(event), name: "handleEvent test" } | |
a.name = "test"; | |
// should not set "test" | |
a.name = "updated"; | |
// Object { previous: "test", newValue: "updated" } | |
var b = Object.assign({}, a); | |
b.name = 'B'; | |
b.onpropertychange("name", function(event) { | |
console.log(event); | |
}); | |
b.name = "Big B"; | |
// propertychange { | |
// detail: Object { previous: "B", newValue: "Big B" } | |
// target: null | |
// } | |
// Add another listener to A... | |
a.onpropertychange("name", function (event) { | |
console.warn("overridden"); | |
console.log(this); | |
console.log(event.detail); | |
console.log(this.name); | |
}); | |
var newName = "Big A"; | |
a.name = newName; | |
// overridden | |
// Object { name: Getter & Setter } | |
// Object { previous: "updated", newValue: "Big A" } | |
// Big A | |
console.log(a.name); | |
// Big A | |
a.stop('name'); | |
console.assert(a.name === newName, `should preserve name, "${newName}"`); | |
a.name = "next"; | |
console.log(a.name); | |
// next | |
a.name = "overridderrrnnnn"; | |
console.log(a.name); | |
// overridderrrnnnn | |
// A form element... | |
var input = document.createElement("input"); | |
input.defaultValue = "default value"; | |
input.onpropertychange("value", function (e) { | |
console.log( e ); | |
}); | |
input.setAttribute("value", "another value"); | |
console.log( input.value ); | |
// default value | |
input.value = "input.value"; | |
// propertychange { | |
// detail: Object { previous: "default value", newValue: "input.value" } | |
// target: null | |
// } | |
console.log( input.outerHTML ); | |
// <input value="another value"> | |
// An Array... | |
var a = ['a','b','c']; | |
a.onpropertychange('length', function (e) { | |
console.log(e); | |
}); | |
a.push('d', 'e'); | |
// Uncaught TypeError: can't redefine non-configurable property "length" | |
// Could be that defineProperty uses get() and set() and therefore cannot use | |
// configurable: true... | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment