I have the following object:
mind = {
queries: [],
actions: []
};
and I update queries and actions according to another function.
I wanted to detect every time they're being updated and changed, and i've heard about MutationObserver, so I tried to call it:
var muob = (window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver);
var ob = new muob(function(m) {
console.log('It works!');
});
ob.observe(mind, { subtree: true });
But it doesn't work. I get in return:
Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
What's wrong with my code?
MutationObserver is only something that works for DOM elements, not objects:
var ob = new MutationObserver(function(m) {
console.log('It works!');
});
ob.observe(mind, { childList: true });
mind.textContent = 'foo';
<div id="mind"></div>
For what you're doing, you can make the queries and actions properties have methods to update the arrays instead, eg:
const mind = {
_queries: [],
_actions: [],
queries: {
push(...args) {
console.log('Push detected');
mind._queries.push(...args);
},
get() {
return mind._queries;
}
},
actions: {
push(...args) {
console.log('Push detected');
mind._actions.push(...args);
},
get() {
return mind._actions;
}
}
};
mind.queries.push('foo');
console.log(mind.queries.get());
Or, using a Proxy:
const handler = {
set(obj, prop, newVal) {
console.log('Change detected');
return obj[prop] = newVal;
},
get(obj, prop) {
return obj[prop];
}
};
const mind = {
queries: new Proxy([], handler),
actions: new Proxy([], handler),
};
mind.queries.push('foo');
console.log(mind.queries);
(the above snippet logs Change detected twice because it has to update both the 0 property on the array and change the array's .length)
Still, this is pretty odd - it would be much more elegant if, at the location in the code where you change the array, you also call another function (the It works! part) to indicate an update has occurred.
MutationObserver is for DOM elements, not JavaScript objects.
There is no equivalent for JavaScript objects,¹ but you can use a combination of Proxy objects and accessor properties to get a notification of any change to those arrays. You'd use the Proxy to know when the arrays were modified, and by making x and y accessor properties, you could know when they were changed (and use that as the opportunity to wrap them in proxies).
Here's a rough sketch:
const mind = (() => {
function wrap(array) {
return new Proxy(array, {
set(target, propName, value, receiver) {
beforeChange(target, propName, value, receiver);
const result = Reflect.set(target, propName, value);
afterChange(target, propName, value, receiver);
return result;
}
// ...you may want other traps here...
});
}
function beforeChange(target, name, value, receiver) {
console.log("beforeChange", name, value);
}
function afterChange(target, name, value, receiver) {
console.log("afterChange", name, value);
}
let queries = wrap([]);
let actions = wrap([]);
return {
get queries() {
return queries;
},
set queries(value) {
beforeChange(queries, "*queries*", value);
queries = wrap(value);
afterChange(queries, "*queries*", value);
},
get actions() {
return queries;
},
set queries(value) {
beforeChange(queries, "*actions*", value);
queries = wrap(value);
afterChange(queries, "*actions*", value);
}
};
})();
mind.queries.push(1);
mind.actions.push("two");
console.log(mind.actions);
mind.actions[0] = "TWO";
console.log(mind.actions);
mind.queries = [];
mind.queries[10] = "ten";
console.log(mind.queries);
.as-console-wrapper {
max-height: 100% !important;
}
¹ For a brief time there was going to be Object.observe, but it was abandoned in favor of Proxy.
Related
Is it possible for a JS field decorator to change its value?
A simplified use case would be something like this:
const addItem = (newValue) => {
return function (target) {
target.value.push(newValue);
};
};
class Test {
#addItem(4)
static values = [1,2,3];
}
const test = new Test();
console.log(test.constructor.values) // [1,2,3,4]
Using the following experimental decorators:
'#babel/plugin-proposal-decorators',
{
version: '2018-09',
decoratorsBeforeExport: true,
},
End goal is to make a decorator to inject tailwind style sheets into a lit elements static styles. Currently using a mixin for this but just doing this for fun and to learn whats possible with decorators.
Update to Barmars comments
When trying to return a value from the inner function, I end up getting an error:
export const addItem = (value) => {
return function (target) {
return [value];
};
};
Uncaught TypeError: An element descriptor's .kind property must be either "method" or "field", but a decorator created an element descriptor with .kind "undefined"
Looking at the documentation, the variables getting passed to each of these functions doesn't seem to match either.
function logged(value, { kind, name }) {
if (kind === "field") {
return function (initialValue) {
console.log(`initializing ${name} with value ${initialValue}`);
return initialValue;
};
}
}
When running that example, the 2nd parameter to logged() is undefined. "initialValue" also is an object, not the value:
Object { kind: "field", key: "styles", placement: "own", descriptor: {…}, initializer: value(), … }
Nicolo Ribaudo was able to help me over on Babel's discussions. The correct way to do this is to use the initializer function:
const addItem = (newValue) => {
return function (target) {
const { initializer } = target;
target.initializer = function () {
return [
...initializer.call(this),
newValue,
];
};
};
};
class Test {
#addItem(4)
static values = [1,2,3];
}
const test = new Test();
console.log(test.constructor.values) // [1,2,3,4]
I'd like to redefine an existing property inside a class (it's for experimenting purposes; I know I shouldn't).
For some reason, the following code works in a browser (Chrome), but not Node.js (v18.12.0).
function re(instance, name, val) {
let _value = val;
Object.defineProperty(instance, name, {
get: () => { return _value },
set: (v) => { return _value = v }
})
return val;
}
class A {
prop = re(this, 'prop', 456)
}
const a = new A()
console.log(a.prop)
The Chrome console output would be 456, but Node.js will be like nope, no redefining today, instead take this: TypeError: Cannot redefine property: prop. Which is sad. I tested on my PC plus at some online Node.js interpreter (replit.com).
You need to the provide configurable attribute.
function re(instance, name, val) {
let _value = val;
Object.defineProperty(instance, name, {
get: () => { return _value },
set: (v) => { return _value = v },
configurable: true
}, )
return val;
}
class A {
prop = re(this, 'prop', 456)
}
const a = new A()
console.log(a.prop)
You can refer to the MDN documentation: TypeError: can't redefine non-configurable property "x"
I use a Proxy object with the idea that whenever a property gets updated, some other side-effect can also be initiated. I don't want to initiate the side effects in the many places that properties get set (DRY principle).
Somewhat contrived example code:
const session = new Proxy(
{ id: undefined as number, age: undefined as number}, // =target
{
set: (target, property, value): boolean => {
switch (property) {
case 'id': {
target[property] = value;
this.notifyIdWasUpdated(value);
return true;
}
case 'age': {
target[property] = value;
this.updateAgeDisplay(value);
return true;
}
default: {
return false;
}
}
}
}
);
My problem is that when I use my IDE's refactoring to change a property name (key) of the target object (e.g. age), the string constants in the case statements (e.g. 'age') don't get updated as well (potentially causing a bug).
Question: Is there a way to dynamically get a string value 'key' from an expression obj.key in the case statement (which would then be refactoring proof)? (Sort of the inverse of the ['key'] accessor, in a way...) Alternatively, can you suggest another way to structure the above code to guard against this sort of programmer oversight?
I have found Get object property name as a string, but wonder if there is a less "iffy" solution - IMHO the tradeoff between a potential problem and adding a lot of code to guard against it is not worth it. (Many techniques seem to iterate through all keys and match on either property type or value; these will not be safe enough.)
Typescript's documentation seems to say that metadata emission for reflection-like use is not yet officially adopted. Also not worth it IMHO to add a whole experimental library just for this.
You can try to use keyof here.
interface Session {
id: number
age: number
}
const session1 = new Proxy(
{ id: 0, age: 0 } as Session,
{
set: (target, property: keyof Session, value): boolean => {
switch (property) {
case 'id': {
target[property] = value;
this.notifyIdWasUpdated(value);
return true;
}
case 'age': {
target[property] = value;
this.updateAgeDisplay(value);
return true;
}
default: {
return false;
}
}
}
}
);
This will not be renamed automatically, but typescript will show error if property in case doesn't exist in Session.
The following case should allow automatic rename:
interface Session {
id: number
age: number
}
type Handlers<Model> = {
[Key in keyof Model]: (newValue: Model[Key]) => void;
}
// Partial<Handlers<Session>> in case you don't want to handle each property
const handlers: Handlers<Session> = {
id: () => { },
age: () => { },
}
const session = new Proxy(
{ id: 0, age: 0 } as Session,
{
set: (target, property: keyof Session, value): boolean => {
const handler = handlers[property];
if (handler) {
handler(value)
return true;
}
return false;
}
}
);
The simples solution would be something like this:
function nameof<TType>(selector: (t?: TType) => any) {
const match = selector
.toString()
.match(/=>\s*(?:[a-zA-Z0-9_$]+\.?)*?([a-zA-Z0-9_$]+)$/);
if (match) {
return match[1];
}
return undefined;
}
interface MyType {
id: any;
age: number;
}
const session = new Proxy(
{ id: undefined as number, age: undefined as number }, // =target
{
set: (target, property, value): boolean => {
switch (property) {
case nameof<MyType>((t) => t.id): {
target[property] = value;
this.notifyIdWasUpdated(value);
return true;
}
case nameof<MyType>((t) => t.age): {
target[property] = value;
this.updateAgeDisplay(value);
return true;
}
default: {
return false;
}
}
},
}
);
DEMO
NOTE: Careful, if you target ES5! Arrow function is transpiled into regular function with return so that regex will not work, you have to change the regex.
Although another answer was chosen, the problem with the given sample code is on a somewhat higher level of abstraction. Since the session object encapsulates a number of properties, the session object should be handled as a unit and not the properties separately. (There is probably a code smell name or some other warning against this...)
The sample would then simply be:
session = new Proxy(
{ id: undefined, age: undefined}, // =target
{
set: (target, property, value): boolean => {
if (typeof property === 'string' && Object.keys(target).includes(<string>property)) {
target[property] = value;
doSideEffects(target);
return true;
} else {
return false;
}
},
}
);
This simplifies the handler in the Proxy.
(I'm the OP. In my case, it has now also simplified the side effect code considerably. I guess the rubber duck effect came into play...)
I would like to write a proxy object to automatically print errors when calling some property in original object which is not found.
const proxyObjectFn = () => {
const _obj = Object.assign({}, originalObject);
const get = (key) => {
const value = _obj[key];
if (value === undefined) {
console.error(`${key} not found`);
}
return value;
};
return {
get,
};
};
const proxyObject = proxyObjectFn();
export default proxyObject;
// caller
proxyObject.get('someProperty')
This works, but is there any elegant way so that I can call through proxyObject.someProperty instead of proxyObject.get('someProperty')?
Update
Let me make it more specific. Actually I am writing a translation object.
Original object may be from json, like { "HELLO_KEY": "Hello World" }. I am to call like { label: _t.SOME_I18N_KEY } in UI display code, assuming _t is the proxy object above. I can print the warning to tell me there is missing translation.
You can use the Proxy object:
const handler = {
get: (obj, prop) => {
if(!obj.hasOwnProperty(prop)) console.error(`${prop} not found`);
return obj[prop];
}
};
const _t = new Proxy({ "HELLO_KEY": "Hello World" }, handler);
console.log(_t.HELLO_KEY);
console.log(_t.SOME_NONEXISTENT_KEY);
I have this code in js bin:
var validator = {
set (target, key, value) {
console.log(target);
console.log(key);
console.log(value);
if(isObject(target[key])){
}
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
if i do proxy.inner.salary = 555; it does not work.
However if i do proxy.firstName = "Anne", then it works great.
I do not understand why it does not work Recursively.
http://jsbin.com/dinerotiwe/edit?html,js,console
You can add a get trap and return a new proxy with validator as a handler:
var validator = {
get(target, key) {
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], validator)
} else {
return target[key];
}
},
set (target, key, value) {
console.log(target);
console.log(key);
console.log(value);
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
A slight modification on the example by Michał Perłakowski with the benefit of this approach being that the nested proxy is only created once rather than every time a value is accessed.
If the property of the proxy being accessed is an object or array, the value of the property is replaced with another proxy. The isProxy property in the getter is used to detect whether the currently accessed object is a proxy or not. You may want to change the name of isProxy to avoid naming collisions with properties of stored objects.
Note: the nested proxy is defined in the getter rather than the setter so it is only created if the data is actually used somewhere. This may or may not suit your use-case.
const handler = {
get(target, key) {
if (key == 'isProxy')
return true;
const prop = target[key];
// return if property not found
if (typeof prop == 'undefined')
return;
// set value as proxy if object
if (!prop.isProxy && typeof prop === 'object')
target[key] = new Proxy(prop, handler);
return target[key];
},
set(target, key, value) {
console.log('Setting', target, `.${key} to equal`, value);
// todo : call callback
target[key] = value;
return true;
}
};
const test = {
string: "data",
number: 231321,
object: {
string: "data",
number: 32434
},
array: [
1, 2, 3, 4, 5
],
};
const proxy = new Proxy(test, handler);
console.log(proxy);
console.log(proxy.string); // "data"
proxy.string = "Hello";
console.log(proxy.string); // "Hello"
console.log(proxy.object); // { "string": "data", "number": 32434 }
proxy.object.string = "World";
console.log(proxy.object.string); // "World"
I published a library on GitHub that does this as well. It will also report to a callback function what modifications have taken place along with their full path.
Michal's answer is good, but it creates a new Proxy every time a nested object is accessed. Depending on your usage, that could lead to a very large memory overhead.
I have also created a library type function for observing updates on deeply nested proxy objects (I created it for use as a one-way bound data model). Compared to Elliot's library it's slightly easier to understand at < 100 lines. Moreover, I think Elliot's worry about new Proxy objects being made is a premature optimisation, so I kept that feature to make it simpler to reason about the function of the code.
observable-model.js
let ObservableModel = (function () {
/*
* observableValidation: This is a validation handler for the observable model construct.
* It allows objects to be created with deeply nested object hierarchies, each of which
* is a proxy implementing the observable validator. It uses markers to track the path an update to the object takes
* <path> is an array of values representing the breadcrumb trail of object properties up until the final get/set action
* <rootTarget> the earliest property in this <path> which contained an observers array *
*/
let observableValidation = {
get(target, prop) {
this.updateMarkers(target, prop);
if (target[prop] && typeof target[prop] === 'object') {
target[prop] = new Proxy(target[prop], observableValidation);
return new Proxy(target[prop], observableValidation);
} else {
return target[prop];
}
},
set(target, prop, value) {
this.updateMarkers(target, prop);
// user is attempting to update an entire observable field
// so maintain the observers array
target[prop] = this.path.length === 1 && prop !== 'length'
? Object.assign(value, { observers: target[prop].observers })
: value;
// don't send events on observer changes / magic length changes
if(!this.path.includes('observers') && prop !== 'length') {
this.rootTarget.observers.forEach(o => o.onEvent(this.path, value));
}
// reset the markers
this.rootTarget = undefined;
this.path.length = 0;
return true;
},
updateMarkers(target, prop) {
this.path.push(prop);
this.rootTarget = this.path.length === 1 && prop !== 'length'
? target[prop]
: target;
},
path: [],
set rootTarget(target) {
if(typeof target === 'undefined') {
this._rootTarget = undefined;
}
else if(!this._rootTarget && target.hasOwnProperty('observers')) {
this._rootTarget = Object.assign({}, target);
}
},
get rootTarget() {
return this._rootTarget;
}
};
/*
* create: Creates an object with keys governed by the fields array
* The value at each key is an object with an observers array
*/
function create(fields) {
let observableModel = {};
fields.forEach(f => observableModel[f] = { observers: [] });
return new Proxy(observableModel, observableValidation);
}
return {create: create};
})();
It's then trivial to create an observable model and register observers:
app.js
// give the create function a list of fields to convert into observables
let model = ObservableModel.create([
'profile',
'availableGames'
]);
// define the observer handler. it must have an onEvent function
// to handle events sent by the model
let profileObserver = {
onEvent(field, newValue) {
console.log(
'handling profile event: \n\tfield: %s\n\tnewValue: %s',
JSON.stringify(field),
JSON.stringify(newValue));
}
};
// register the observer on the profile field of the model
model.profile.observers.push(profileObserver);
// make a change to profile - the observer prints:
// handling profile event:
// field: ["profile"]
// newValue: {"name":{"first":"foo","last":"bar"},"observers":[{}
// ]}
model.profile = {name: {first: 'foo', last: 'bar'}};
// make a change to available games - no listeners are registered, so all
// it does is change the model, nothing else
model.availableGames['1234'] = {players: []};
Hope this is useful!
I wrote a function based on Michał Perłakowski code. I added access to the path of property in the set/get functions. Also, I added types.
const createHander = <T>(path: string[] = []) => ({
get: (target: T, key: keyof T): any => {
if (key == 'isProxy') return true;
if (typeof target[key] === 'object' && target[key] != null)
return new Proxy(
target[key],
createHander<any>([...path, key as string])
);
return target[key];
},
set: (target: T, key: keyof T, value: any) => {
console.log(`Setting ${[...path, key]} to: `, value);
target[key] = value;
return true;
}
});
const proxy = new Proxy(obj ,createHander<ObjectType>());