I need to create an object that stores another objects. Each property of the big object has two properties 'value' and 'callback'.
let bigObj = {
first: {
value: true,
callback: () => {}
},
second: {
value: false,
callback: () => {}
}, {...}
}
I want to be able to get and change the value property by using bigObj.first / bigObj.first = "false", and the callback.. through the classic method: bigObj.first.callback = () => {}.
Each time the property 'value' is changed, I want to call its callback function.
Here's what I did
var proxy = new Proxy({
first: {
value: true,
callback: () => {}
}
}, {
get(target, key) {
return key in target ? target[key].value : null;
},
set(target, key, value) {
target[key] ? target[key].value = value : target[key] = {value, callback: () => {}};
key !== 'callback' && target[key].callback();
return true;
}
});
The problem is that I can not change the callback property.
proxy.first.callback = () => console.log('new cb'); // won't do anything.
Do you have any ideas on how I could change the code so it would work?
Thank you.
The way you have it set up, proxy.first is returning a boolean. So then proxy.first.callback = ends up being false.callback = or true.callback =. These at least don't throw exceptions, but they're useless. If the value was an object instead of a boolean, you could make the value itself be a proxy, but you can't create a proxy with a non-object as the target.
Another option would be to have a special value with which you set first, that tells it to insert the callback. Below is an example, where if you pass in an object like {callback: () => {}}, then it will insert that as the callback. But anything else it will get set as the value.
var proxy = new Proxy({
first: {
value: true,
callback: () => {}
}
}, {
get(target, key) {
return key in target ? target[key].value : null;
},
set(target, key, value) {
if (value && value.callback) {
target[key] ? target[key].callback = value.callback : target[key] = {value: null, callback: value.callback};
return true;
} else {
target[key] ? target[key].value = value : target[key] = {value, callback: () => {}};
target[key].callback();
return true;
}
}
});
proxy.first = {callback: () => console.log('got a callback')};
proxy.first = false;
Related
I would like to observe whenever a property of a third party object is changed. I'm taking the approach of assigning a custom setter but my console.log below is never invoked. Why is that? Is there a better approach?
const foo = { a: 1, b: 2 };
Object.assign(foo, {
set user(user) {
foo.user = user;
console.log(">>>>>> user was changed", user);
},
});
// Desired behaviour
foo.user = "asdf"; // >>>>>> user was changed asdf
delete foo.user; // >>>>>> user was changed undefined
foo.user = "asdf1" // >>>>>> user was changed asdf1
Please note, I need to mutate foo I cannot wrap a proxy around foo and return that because it is a third party library which mutates .user internally
I've found a way, pretty hacky as it is
const foo = { a: 1, b: 2 };
let underlyingValue = foo.user
Object.defineProperty(foo, "user", {
get() {
return underlyingValue
},
set(user) {
underlyingValue = user;
console.log(">>>>>> user was changed", user);
},
enumerable: true
});
foo.user = "asdf";
console.log(foo)
I've made this into a generic function below 👇
/** Intercepts writes to any property of an object */
function observeProperty(obj, property, onChanged) {
const oldDescriptor = Object.getOwnPropertyDescriptor(obj, property);
let val = obj[property];
Object.defineProperty(obj, property, {
get() {
return val;
},
set(newVal) {
val = newVal;
onChanged(newVal);
},
enumerable: oldDescriptor?.enumerable,
configurable: oldDescriptor?.configurable,
});
}
// example usage 👇
const foo = { a: 1 };
observeProperty(foo, "a", (a) => {
console.log("a was changed to", a);
});
foo.a = 2; // a was changed to 2
Also available in typescript
🚨 Edit: This will break if the property is deleted eg delete foo.user. The observer will be removed and the callback will stop firing. You will need to re-attach it.
#david_adler ... when I commented ...
"Is the latter a special case or does the OP need a somehow more generic observation approach?"
... I thought of the most generic solution one could come up with in terms of changing/mutating an existing object entirely into an observable variant of itself.
Such a solution also would be more close to what the OP did ask for ...
"I would like to observe whenever a property of a third party object is changed"
Thus the next provided approach keeps the objects appearance and behavior and also does not introduce additional (e.g. Symbol based) keys.
function mutateIntoObservableZombie(obj, handlePropertyChange) {
const propertyMap = new Map;
function createAccessors(keyOrSymbol, initialValue, handler) {
return {
set (value) {
propertyMap.set(keyOrSymbol, value);
handler(keyOrSymbol, value, this);
return value;
},
get () {
return propertyMap.has(keyOrSymbol)
? propertyMap.get(keyOrSymbol)
: initialValue;
},
};
}
function wrapSet(keyOrSymbol, proceed, handler) {
return function set (value) {
handler(keyOrSymbol, value, this);
return proceed.call(this, value);
};
}
function createAndAssignObservableDescriptor([keyOrSymbol, descriptor]) {
const { value, get, set, writable, ...descr } = descriptor;
if (isFunction(set)) {
descr.get = get;
descr.set = wrapSet(keyOrSymbol, set, handlePropertyChange);
}
if (descriptor.hasOwnProperty('value')) {
Object.assign(descr, createAccessors(keyOrSymbol, value, handlePropertyChange));
}
Object.defineProperty(obj, keyOrSymbol, descr);
}
const isFunction = value => (typeof value === 'function');
if (isFunction(handlePropertyChange)) {
const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
const ownDescrSymbols = Object.getOwnPropertySymbols(ownDescriptors);
Object
.entries(ownDescriptors)
.forEach(createAndAssignObservableDescriptor);
ownDescrSymbols
.forEach(symbol =>
createAndAssignObservableDescriptor([symbol, ownDescriptors[symbol]])
);
}
return obj;
}
// third party object (closed/inaccessible code)
const foo = { a: 1, b: 2 };
// custom changes already.
foo.userName = '';
foo.userLoginName = '';
const userNick = Symbol('nickname');
foo[userNick] = null;
console.log('`foo` before descriptor change ...', { foo });
mutateIntoObservableZombie(foo, (key, value, target) => {
console.log('property change ...', { key, value, target });
});
console.log('`foo` after descriptor change ...', { foo });
foo.a = "foo bar";
foo.b = "baz biz";
console.log('`foo` after property change ...', { foo });
foo.userName = '****';
foo.userLoginName = '************#**********';
console.log('`foo` after property change ...', { foo });
foo[userNick] = 'superuser';
console.log('`foo` after symbol property change ...', { foo });
.as-console-wrapper { min-height: 100%!important; top: 0; }
Edit
Since the above approach already is implemented generic and modular it of cause easily can be refactored into a function which allows the exact definition of which property/ies, both string and symbol based, are going to be observed ...
function observePropertyChange(obj, keysAndSymbols, handlePropertyChange) {
const propertyMap = new Map;
function createAccessors(keyOrSymbol, initialValue, handler) {
return {
set (value) {
propertyMap.set(keyOrSymbol, value);
handler(keyOrSymbol, value, this);
return value;
},
get () {
return propertyMap.has(keyOrSymbol)
? propertyMap.get(keyOrSymbol)
: initialValue;
},
};
}
function wrapSet(keyOrSymbol, proceed, handler) {
return function set (value) {
handler(keyOrSymbol, value, this);
return proceed.call(this, value);
};
}
function createAndAssignObservableDescriptor(keyOrSymbol, descriptor) {
const { value, get, set, writable, ...descr } = descriptor;
if (isFunction(set)) {
descr.get = get;
descr.set = wrapSet(keyOrSymbol, set, handlePropertyChange);
}
if (descriptor.hasOwnProperty('value')) {
Object.assign(descr, createAccessors(keyOrSymbol, value, handlePropertyChange));
}
Object.defineProperty(obj, keyOrSymbol, descr);
}
const isString = value => (typeof value === 'string');
const isSymbol = value => (typeof value === 'symbol');
const isFunction = value => (typeof value === 'function');
if (isFunction(handlePropertyChange)) {
const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
const identifierList = (Array
.isArray(keysAndSymbols) && keysAndSymbols || [keysAndSymbols])
.filter(identifier => isString(identifier) || isSymbol(identifier));
identifierList
.forEach(keyOrSymbol =>
createAndAssignObservableDescriptor(keyOrSymbol, ownDescriptors[keyOrSymbol])
);
}
return obj;
}
// third party object (closed/inaccessible code)
const foo = { a: 1, b: 2 };
// custom changes already.
foo.userName = '';
foo.userLoginName = '';
const userNick = Symbol('nickname');
foo[userNick] = null;
console.log('`foo` before descriptor change ...', { foo });
observePropertyChange(
foo,
['b', 'userLoginName', userNick],
(key, value, target) => { console.log('property change ...', { key, value, target }); },
);
console.log('`foo` after descriptor change ...', { foo });
foo.a = "foo bar";
foo.b = "baz biz";
console.log('`foo` after property change ...', { foo });
foo.userName = '****';
foo.userLoginName = '************#**********';
console.log('`foo` after property change ...', { foo });
foo[userNick] = 'superuser';
console.log('`foo` after symbol property change ...', { foo });
.as-console-wrapper { min-height: 100%!important; top: 0; }
const self = {
element: document.querySelector(selector),
html: () => self.element,
on: (event, callback) => {
self.element.addEventListener(event, callback);
},
style: {
alignContent: (property) => {
return (property === null) ? self.element.style.alignContent : self.element.style.alignContent = property;
}
}
}
I am trying to make it so I have quick access to all CSS style properties with jQuery like selectors it should work as: select('h1').style.alignContent('center'), but the problem is that I would have to make a seperate function for each style property in order for this method to work, is there a way to solve this problem without duplicating a lot of code?
//Duplication example
color: (property) => {
return (property === null) ? self.element.style.color : self.element.style.color = property;
}
One way to do this is with a Proxy (mdn):
let elemWrapper = selector => {
let element = document.querySelector(selector);
return {
element,
html: () => element,
on: (event, callback) => {
element.addEventListener(event, callback);
},
style: new Proxy({}, {
get: (obj, prop) => {
// The user called a function named "prop"
// We need to return a function that sets the style property named "prop"
return cssValue => element.style[prop] = cssValue;
}
})
};
};
let bodyElem = elemWrapper('body');
bodyElem.style.backgroundColor('cyan');
Here to prove the concept I've set the body element's background colour using a dynamically named function.
The big downside to this approach is the poor performance of Proxies (an excellent read on Proxy performance is available here).
This means it may be quicker to simply compile a list of all css property names, and define a function for each (never using Proxies). The following code compiles all css property names, to serve as a starting point:
console.log(Object.keys(document.body.style));
You can use a Proxy to intercept all attempts to get a property.
let selector = '#test';
const self = {
element: document.querySelector(selector),
html: () => self.element,
on: (event, callback) => {
self.element.addEventListener(event, callback);
},
style: new Proxy(Object.create(null), {
get(target, prop, receiver) {
if (self.element.style.hasOwnProperty(prop)) {
return val => {
if (val != null) {
self.element.style[prop] = val;
} else {
return self.element.style[prop];
}
}
}
throw Error("No such property exists: " + prop);
}
})
};
self.style.color('red')
console.log("Color:", self.style.color());
<div id="test">
This is a test
</div>
You can also wrap this into a general function like so:
const getEnhancedElement = arg => {
const element = /Element/.test(Object.prototype.toString.call(arg)) ? arg
: document.querySelector(arg);//accept a HTMLElement or a selector
return {
element,
html: () => element,
on: (event, callback) => {
element.addEventListener(event, callback);
},
style: new Proxy(Object.create(null), {
get(target, prop) {
if (element.style.hasOwnProperty(prop)) {
return val => {
if (val != null) {//set value
element.style[prop] = val;
} else {//get value
return element.style[prop];
}
}
}
throw Error("No such property exists: " + prop);
}
})
};
};
let test = getEnhancedElement("#test");
test.style.color('red')
console.log("Color:", test.style.color());
test.style.textAlign('center');
<div id="test">
This is a test
</div>
I would have something like this:
style: {
chnageStyle: (propertyName, propertyVal) => {
return (propertyName === null) ? self.element.style[propertyName] : self.element.style[propertyName] = propertyVal;
}
}
Then you can call this:
style.changeStyle('alignContent','center');
style.changeStyle('color','orange');
I'm using spyOn, in a jasmine test, to "listen" to a function call from a service,
that function returns an Observable.
I'm getting the error unexpected token U JSON;
The error is generated from the component line:
this.config = JSON.parse(localStorage.getItem('configuration'));
The localStorage item was JSON.stringified;
I understand that this error is usually thrown when JSON.parse = undefined,
So I tried to set the variable within my test i.e.
component.config = mockConfig;
So..
// Storage Mock
function storageMock() {
var storage = {};
return {
setItem: function(key, value) {
storage[key] = value || '';
},
getItem: function(key) {
return key in storage ? storage[key] : null;
},
removeItem: function(key) {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: function(i) {
var keys = Object.keys(storage);
return keys[i] || null;
}
};
}
let mockConfig = JSON.stringify({
base_url:"http://image_url/",
poster_sizes:['w9', 'w100']
})
//Set storage
let m = storageMock()
m.setItem('configuration', mockConfig)
it('Should set items array with values from MoviesService', () => {
component.config = JSON.parse(m.getItem('configuration'));
let spy = spyOn(moviesService, 'getPreview').and.callFake(()=>{
return Observable.from([[{id1: 1, title: 'a'}, {id1: 2, title: 'b'}]])
})
component.ngAfterViewInit();
expect(component.items.length).toBeGreaterThan(0);
});
For anyone who come across this problem I got this working by by placing this code in the beforeEach wrapper
Object.defineProperty(window, 'localStorage', { value: m });
Basically it uses my mock localStorage variable instead of the one from the window object.
I know this may be a silly question but I'm new to javascript so would appreciate some guidance.
I have the following notificationSocketEventHandler object created:
const notificationSocketEventHandler = Object.create(socketHandlerProto, {
validators: {
created: [],
destroyed: [],
loadedFromSocket: [],
updated: [],
addedto: {
relation: []
},
removedfrom: {
}
},
created: function (data) {
if (this.validateProfileData(data, validators.created)) {} else {}
},
destroyed: function (data) {},
updated: function (data) {},
loadedFromSocket: function (data) {
console.log('Loaded from socket')
console.log(data)
}
})
This event handler is being used to listen for notifications from a socket
so and is set like this
$.globals.socket.on('notifications',notificationSocketEventHandler);
the event prototype is defined as such:
$.globals.socket = {
events: {},
on: function (attr, func) {
if (!attr) return false
this.events[attr] = this.events[attr] || [];
this.events[attr].push(func)
return true
},
remove(attr, func) {
if (!events[attr]) return false
this.events[attr].forEach(function (f, indx) {
if (f === func) {
events[attr].slice(indx, 1)
}
})
},
trigger: function (attr, thisArg, paramArgs) {
if (Array.isArray(attr) && attr.length) {
var obj = this.events[attr[0]]
for (var i = 1; i < attr.length; i++) {
if (!obj) return
obj = obj[attr[i]]
}
if (typeof obj == 'function') obj.apply(thisArg, paramArgs)
if (Array.isArray(obj)) {
obj.forEach(function (c) {
if (typeof c == 'function') c.apply(thisArg, paramArgs)
})
}
return
}
if(this.events[attr]){
console.log(this.events[attr])
this.events[attr].forEach(function (f) {
if (typeof f === 'function')
f.apply(thisArg, paramArgs)
})
}
}
}
The problem I am having is that after the notificationSocketEventHandler object gets passed to the $.globals.socket.on function and ultimately pushed into the events object, the properties of the notificationSocketEventHandler such as created, destroyed,loadedFromSocket which are defined as functions before being passed to the $.globals.socket.on function, suddenly become `undefined' once within the events object, why is this?
Object.create is a little confusing - you can't use a normal object as the second parameter, it has to be an object of 'property descriptors'. To work properly your code would need to be formatted along these lines:
const notificationSocketEventHandler = Object.create(socketHandlerProto, {
'validators': {
value: {
'created': {
value: []
},
'destroyed': {
value: []
},
'loadedFromSocket': {
value: []
}
}
}
});
Unless you iterate over those properties it's going to be tedious. You would be much better off avoiding Object.create and just making an object normally:
const notificationSocketEventHandler = {
created: [],
destroyed: [],
etc...
}
or adding the properties to the constructor's prototype (as an aside, people usually make constructor names begin with an uppercase letter so it's immediately obvious they're constructors - saves you having to add 'Proto' at the end of the name for one thing. Anyway):
var SocketHandler = function {
this.created = [];
this.destroyed = []
etc...
}
const notificationSocketEventHandler = new SocketHandler
or
var SocketHandler = {};
SocketHandler.prototype.created = [];
SocketHandler.prototype.destroyed = [];
etc...
I read a lot about JS accessors here and figure out this gonna be good for me:
This is what I used for local fields:
TYPE_DEFAULT_VALUE= {
number: 0,
string: "",
array: [],
object: {},
};
typeOf = function (object) {
if (typeof object === "number" && isNaN(object))
return NaN;
try {
return Object.prototype.toString.call(object).slice(8, -1).toLowerCase();
}
catch(ex) {
return "N/A";
};
};
getAccessor = function(obj, key, type, defaultValue) {
if (defaultValue === undefined)
defaultValue = TYPE_DEFAULT_VALUE[type] === undefined ? null : TYPE_DEFAULT_VALUE[type];
return {
enumerable: true,
configurable: true,
get: function () {
if (obj[key] === undefined)
obj[key] = defaultValue;
return obj[key];
},
set: function (value) {
if (typeOf(value) === type)
obj[key] = value;
},
};
}
LocalFields = function (fields, object) {
/**
* field properties
* {
* type: [ required ] ( number | string | array | object | ... ),
* defaultValue: [ optional ]
* }
*/
if (! fields)
throw "Too few parameters ...";
if (! object)
object = this;
var obj = this;
var fieldsAccessor = {};
for(key in fields){
field = fields[key];
fieldHandler = key[0].toUpperCase() + key.substr(1);
if(! field.type)
throw "Type not set for field: " + key;
fieldsAccessor[fieldHandler] = getAccessor(obj, fieldHandler, field.type, field.defaultValue)
}
Object.defineProperties(object, fieldsAccessor);
}
Now for each Class I can just call something like:
Person = function(){
new LocalFields({
id: { type: "number" },
name: { type: "string" },
phone: { type: "array" },
}, this);
}
And then like VS getter and setter you'll call:
var alex = new Person();
alex.Name = "Alex Ramsi";
console.clear();
console.info(alex.Name);
this works for all types but there is a problem because getter and setter is the basic operation and what if I want to add an array field and call this append method or even prepend is there anyhow anyway to do that?
For example How can I call:
alex.Phone.append('+1234567890');
That's a good effort but you forgot that there is no append function for array list!
You can use push and any other array functionality. Check it again;