i'm trying to watch an objects value, and if it changes, so some stuff..
So this is my object,
var list = [{
object: undefined,
index: 0,
src: 'url here',
active: { val: 0 }
}]
So I made active an additional object as I create a new object from the above, but make the active value the value from above, this keeps a reference of this object between the 2 objects.
var newList = [];
newList.push({
object: undefined,
index: 0,
src: list[i].src,
active: list[i].active // Use reference to old list
});
So i'm trying to watch the active value like so:
(list.active).watch('val', function() {
if (list.active.val === 1) {
console.log('active');
list.object.classList.add('active');
} else {
console.log('not active');
list.object.classList.remove('active');
}
});
However it appears that when I watch this value it is being removed, as if I console.log out the list, then the value is set to undefined! I'm changing the value of list.active.val after adding the watch events.
Here is the Polyfill i'm using for the watch functionality.
// object.watch
if (!Object.prototype.watch) {
Object.defineProperty(Object.prototype, "watch", {
enumerable: false,
configurable: true,
writable: false,
value: function (prop, handler) {
var oldval = this[prop],
newval = oldval,
getter = function () {
return newval;
},
setter = function (val) {
oldval = newval;
return newval = handler.call(this, prop, oldval, val);
};
if (delete this[prop]) { // can't watch constants
Object.defineProperty(this, prop, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
});
}
// object.unwatch
if (!Object.prototype.unwatch) {
Object.defineProperty(Object.prototype, "unwatch", {
enumerable: false,
configurable: true,
writable: false,
value: function (prop) {
var val = this[prop];
delete this[prop]; // remove accessors
this[prop] = val;
}
});
}
Edit 1
Added the proxy-observe Polyfill however this doesn't appear to be watching still, I have added it like so:
list[0] = Object.observe(list[0], function(changeset) {
console.log('changed');
});
list[0].active contains the object { val: 0 }, so it should be observicing this object.
Not getting any errors, it's just doing nothing, ideas?
You don't need to implement a custom watch/unwatch features,
Ecmascript 2015 already provides a specific api:
Proxy
There are a plenty of polyfills to make it working on legacy browsers.
There was a proposal called Object.Observe to address what you need and you can find a Proxy porting here: https://github.com/anywhichway/proxy-observe
Follows a basic working example:
// Your Observable target
const target = Object.create(null);
const observer = {
set(target, key, value) {
console.log(`Should we set '${value}' as value of '${key}' property?`)
target[key] = value;
},
};
const observable = new Proxy(target, observer);
observable.someKindOfProperty = 'Hello World';
Related
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;
I'm fairly new to getters and setters and am looking for a way to listen for changes in an object to store the data immediately, without calling a Save() function everytime a value gets changed. This is how I do it right now:
var myObject = {
Data: {
enabled: true,
show: false
},
Save: function () {
//store myObject.Data to local storage
},
Load: function () {
//load data from local storage and assign it to myObject.Data
},
doSomething: function () {
myObject.Load();
if (myObject.Data.enabled) {
myObject.Data.show = true;
myObject.Save();
}
}
Now I would like to optimize this code so everytime a property in myObject.Data is changed, myObject.Save() is executed. The problem I'm experiencing is that it seems only possible to define a getter for a property that has just one value, but not for a property that is an object itself.
var myObj = {
_Data: {
a: 0,
b: 1,
c: 3
},
set Data (a) {
console.log(a);
}
};
myObj.Data.a = 2;
This obviously doesn't work since myObj.Data is not an object and doesn't have the same properties as myObj._Data.
Thanks in advance for any help.
You are likely interested in the Proxy object.
I used a very simple debounce function callHandler in order to avoid calling the onSet method dozens of times during array modifications. Otherwise, [1, 2, 3].splice(0, 1) would call the set handler once per item in the original array.
'use strict';
var myObject = {
Data: {
a: [1, 2, 3],
b: {c: ['test']}
},
Save: function() {
console.log('Save called');
},
}
function recursiveProxy(target, onSet) {
// For performance reasons, onSet will only be called one millesecond
// after the set handler has last been called.
var timeout;
function callHandler() {
clearTimeout(timeout);
timeout = setTimeout(onSet, 1);
}
var recursiveHandler = {
get: function(target, property) {
// If the property is something that could contain another object,
// we want to proxy it's properties as well.
if (typeof target[property] == 'object' && target[property] != null) {
return new Proxy(target[property], recursiveHandler);
}
return target[property];
},
set: function(target, property, value) {
console.log('Set called - queueing onSet');
callHandler();
target[property] = value;
return true;
}
}
return new Proxy(target, recursiveHandler);
}
myObject.Data = recursiveProxy(myObject.Data, myObject.Save);
myObject.Data.a.splice(0, 1);
myObject.Data.b.c[0] = 'test 2';
I believe you are looking for Defining a getter on existing objects using defineProperty
To append a getter to an existing object later at any time, use
Object.defineProperty().
var o = { a:0 }
Object.defineProperty(o, "b", { get: function () { return this.a + 1; } });
console.log(o.b) // Runs the getter, which yields a + 1 (which is 1)
For e.g:
var Data = {
enable: true,
show: false
};
Object.defineProperty(Data, 'doSomething', {
get: function() {
// get something;
},
set: function(something) {
// set something
}
});
What I essentially want to do is this:
Blog.prototype = {
set content(content) {
this.content = JSON.parse(content);
}
}
However, this results in infinite recursion.
I know I can do something like:
set content(content) {
this._content = JSON.parse(content);
},
get content() {
return this._content;
}
However, when I do JSON.stringify(blog), it doesn't include content, but includes _content, which is undesirable.
How can I go about doing this?
Make the "_content" variable non-enumerable.
Blog.prototype = {
set content(newContent) {
Object.defineProperty(this, "_content", {
value: JSON.parse(newContent),
writable: true
});
},
get content() {
return this._content;
}
};
By default, an the "enumerable" flag for an object property is false if not supplied explicitly in the call to defineProperty().
Someday the Symbol type will be universally supported, and it'd be a better choice for this because you can make a guaranteed unique property key that way. If you don't need IE support and can use Symbols:
Blog.prototype = () => {
const internalContent = Symbol("content key");
return {
set content(newContent) {
this[internalContent] = newContent;
},
get content() {
return this[internalContent];
}
};
}();
Symbol-keyed properties are ignored by JSON.stringify() so you don't have to bother with defineProperty(). The nice thing about the Symbol approach is that you don't have to worry about collisions. Every Symbol instance returned from Symbol() is distinct.
Use Set and Get with _content, and implement .toJson() to provide JSON.stringify with content instead of _content.
toJSON() {
return {
content: this._content
}
}
According to MDN .toJSON() role is:
If an object being stringified has a property named toJSON whose value
is a function, then the toJSON() method customizes JSON
stringification behavior: instead of the object being serialized, the
value returned by the toJSON() method when called will be serialized.
Using with a constructor function
function Blog() {}
Blog.prototype = {
set content(content) {
this._content = JSON.parse(content);
},
get content() {
return this._content;
},
toJSON() {
return {
content: this._content
}
}
};
var blog = new Blog();
blog.content = '{ "a": "5" }';
console.log(blog.content);
console.log(JSON.stringify(blog));
Using with ES6 class
class Blog {
set content(content) {
this._content = JSON.parse(content);
}
get content() {
return this._content;
}
toJSON() {
return {
content: this._content
}
}
};
const blog = new Blog();
blog.content = '{ "a": "5" }';
console.log(blog.content);
console.log(JSON.stringify(blog));
I was able to solve this by building off Pointy's answer:
var Blog = function () {
var content;
Object.defineProperty(this, "content", {
get: function() {
return content;
},
set: function(value) {
content = JSON.parse(value);
},
enumerable: true,
});
};
The trick here is the enumerable flag, which is false by default.
I have written a class with a property decorator that sets a flag in the class when ever a decorated property is set. I also want to be able to copy from one instance of the class to another. The problem is that when I set the value of property on one object, the value of the property on another object changes too, as if the property were static. I am new to JavaScript and TypeScript. What did I miss?
Running the text code below will log:
Setting propNum from undefined to 0
testclass.ts:18 Setting propNum from 0 to 123
test.spec.ts:13 t1.propNum = 123
test.spec.ts:14 t2.propNum = 123
t1.propNum should still be zero
Decorator
//
// property decorator to set dirty flag automatically for any decorated property
//
function testProperty( target: any, key: string ) {
// property value
var _val = this[key];
// property getter
function getter() {
return _val;
};
// property setter
function setter( newVal ) {
if ( _val != newVal ) {
console.log( `Setting ${key} from ${_val} to ${newVal}` );
_val = newVal;
this._dirty = true;
}
};
//
// Delete original property and define new property with getter & setter
//
if ( delete this[key] ) {
// Create new property with getter and setter
Object.defineProperty( target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
Test Class
export class TestClass {
private _dirty: boolean;
#testProperty
public propNum: number = 0;
constructor() {
this._dirty = false;
}
public copyFrom( tc: TestClass ) {
this.propNum = tc.propNum;
}
}
Test Code
describe( 'Copy Class Test', () => {
it( 'Copy Test', () => {
var t1 = new TestClass();
var t2 = new TestClass();
t2.propNum = 123;
console.log( `t1.propNum = ${t1.propNum}` );
console.log( `t2.propNum = ${t2.propNum}` );
expect( t1.propNum ).toBe( 0 );
t1.copyFrom( t2 );
expect( t1.propNum ).toBe( 123 );
});
});
The main issue here is that the getter and setter are sharing the same variable instead of getting a value based on the instance.
It's basically the same as doing this:
function TestClass() {
}
var value;
Object.defineProperty(TestClass.prototype, "propNum", {
get: function() { return value; },
set: function(val) { value = val },
enumerable: true,
configurable: true
});
Which causes this to happen:
var a = new TestClass(), b = new TestClass();
a.propNum = 2;
a.propNum === b.propNum; // true, because they're both referencing the same variable
Second issue is that this[key] references a property on the global object.
What you probably want to do is something along these lines (untested code):
function testProperty( target: Object, key: string ) {
const privateKey = "_" + key;
function getter() {
return this[privateKey];
}
function setter( newVal: any ) {
if ( this[privateKey] != newVal ) {
console.log( `Setting ${key} from ${this[privateKey]} to ${newVal}` );
this[privateKey] = newVal;
this._dirty = true;
}
}
Object.defineProperty( target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
Fiddle
var Assertion = function() {
return { "dummy": "data" };
}
Object.defineProperty(Object.prototype, 'should', {
set: function(){},
get: function(){
return new Assertion(this);
}
});
// Insert magic here.
// This needs to be false
console.log(({}).should === undefined);
What options do I have in ES5 to undo a defineProperty call ?
No silly suggestions like Object.defineProperty = function() { } please.
The following Object.defineProperty(Object.prototype, 'should', {})
does not work
and Object.defineProperty(Object.prototype, 'should', { value: undefined })
Throws a Uncaught TypeError: Cannot redefine property: defineProperty in V8
Object.defineProperty(Object.prototype, 'should', {
set: function() {},
get: function() { return undefined; }
});
Throws the same error
delete Object.prototype.should also does not work
In general, you can't undo a defineProperty call, since there's no undo stack or something. The JS engine does not keep track of previous attribute descriptors.
For example,
Object.defineProperty(Object.prototype, 'foo', {
configurable: true,
value: 1,
enumerable: false
});
Object.defineProperty(Object.prototype, 'foo', {
get: function () {
alert('You cannot revert me');
return 2;
},
enumerable: true
});
What you can do is remove or reconfigure an attribute, or overwrite its value. As mentioned in the other answer, the configurable flag is required to be true if you want to remove or reconfigure.
Once a property is defined with configurable:false, you cannot change the configurable flag.
To remove an attribute (this is supposedly what you want to do), use delete:
Object.defineProperty(Object.prototype, 'foo', {
configurable: true, // defaults to false
writable: false,
value: 1
});
delete Object.prototype.foo;
console.log(Object.prototype.hasOwnProperty('foo')); // false
To reconfigure, use defineProperty again and pass a different descriptor:
Object.defineProperty(Object.prototype, 'foo', {
configurable: true,
get: ...
set: ...
});
Object.defineProperty(Object.prototype, 'foo', {
value: undefined
});
console.log({}.foo); // undefined
console.log(Object.prototype.hasOwnProperty('foo')); // true
As shown in this sample, you can use defineProperty to switch between accessor (get/set) and data (value) properties.
To overwrite, use simple assignment. In this case, you need the writable flag to be true. Obviously this does not work with accessor properties. It even throws an exception:
Object.defineProperty(Object.prototype, 'foo', {
configurable: true,
value: 1,
writable: true // defaults to false
});
Object.prototype.foo = undefined;
console.log(Object.prototype.foo); // undefined
console.log(Object.prototype.hasOwnProperty('foo')); // true
Object.defineProperty(Object.prototype, 'foo', {
get: function () {
return 1;
},
writable: true // JS error!
});
Note that writable defaults to false when you use defineProperty, but true when you use the simple syntax o.attr = val; to define a (previously not existing) property.
If you want to undo your last defineProperty or all of them, you can use this class:
(gist here)
class PropertyDescriptorStack {
private readonly descriptors: PropertyDescriptor[] = [];
constructor(private readonly target: Object, private readonly prop: string) {
if (!target || typeof prop !== "string") { // your choice to define ""
throw new Error("PropertySaver: no object or property");
}
}
public push(props: Partial<PropertyDescriptor>): boolean {
this.saveDescriptor(this.target, this.prop);
try {
Object.defineProperty(this.target, this.prop, {
...props,
configurable: true,
});
return true;
}
catch (e) {
console.error(`Error setting property ${this.prop} on ${this.target}: ${e}`);
return false;
}
}
public pop(toStart?: boolean): boolean {
const ind = toStart ? 0 : this.descriptors.length - 1;
const descriptor = this.descriptors[ind];
if (!descriptor) {
return false;
}
this.descriptors.splice(ind, this.descriptors.length - ind);
try {
Object.defineProperty(this.target, this.prop, descriptor);
return true;
}
catch (e) {
console.error(`Error resetting property ${this.prop} on ${this.target}: ${e}`);
return false;
}
}
/**
* Saves the current descriptor of the property in the object in the descriptors stack.
* The descriptor is taken either from the object or from the closest prototype that has this prop.
* If none is found, a new descriptor is generated with the current value.
* #param target
* #param prop
* #returns The found descriptor
*/
private saveDescriptor(target: object, prop: string): PropertyDescriptor {
let ds: PropertyDescriptor | null = null;
for (let o: any = target, ds: PropertyDescriptor = null; o; o = Object.getPrototypeOf(o)) {
ds = Object.getOwnPropertyDescriptor(o, prop);
if (ds) {
break;
}
}
ds = ds || {
configurable: true,
writable: true,
value: target[prop],
enumerable: true
}
this.descriptors.push(ds);
return ds;
}
}