Proxying localStorage for dynamic object insertion - javascript

I want to proxy localStorage setters and getters to parse objects and save them to storage on assignment like I use a regular object.
Pretty straight forward when saving single KV items but complicated(for me) when trying to update a nested object in the storage.
The problem I have is how to get the correct position in the object using the target parameter.
To overcome this problem, I prototyped the proxy object back to localStorage and parse it every time the proxy calls set.
It works like expected but doesn't look like the proper way to do it.
Any advice be appreciated.
Storage.prototype.proxy = {}
Storage.prototype.getAll = function () {
data = {};
Object.keys(this).forEach(key => {
try {
data[key] = JSON.parse(this[key])
} catch {
data[key] = this[key]
}
});
this.proxy = data;
return this.proxy;
}
Storage.prototype.update = function () {
this.clear();
obj = this.proxy;
Object.keys(obj).forEach(key => {
this.setItem(key, JSON.stringify(obj[key]));
});
}
Storage.prototype.dynamic = new Proxy(localStorage.getAll(), handler = {
get(target, prop) {
if (prop == 'isProxy') return true;
if (typeof target[prop] == 'undefined') return;
if (!target[prop].isProxy && typeof target[prop] === 'object') {
target[prop] = new Proxy(target[prop], handler);
}
return Reflect.get(...arguments);
},
set() {
Reflect.set(...arguments);
localStorage.update();
return true;
},
deleteProperty(target, prop) {
if (prop in target) {
delete target[prop];
localStorage.update();
}
}
});
const storage = localStorage.dynamic;
storage.new = {}; //create new key in storage that hold an object
storage.new.one = 1; //create new object in and assign 1 to it
console.log(storage.new.one); // prints 1

Related

Get value of the key in deeply nested object

Let's say I have input object like this one:
const obj = {
deep: {
someKey: 'someValue1!',
veryDeep: {
someOtherKey: 'someOtherValue'
}
},
deep2: {
aa: 'bba'
}
}
I want to write a function that will take this object as first argument and key string as second argument. Then it's gonna loop through obj with recursion and find the value and return it. So if I name this function getObjectValueOfKey it will be called like this: getObjectValueOfKey(obj, 'aa') and it will return 'bba'.
I think I'm close with this code:
const getObjectValueOfKey = (obj, key) => {
const keys = Object.keys(obj)
for (let currKey of keys) {
if (typeof obj[currKey] === 'object') {
getObjectValueOfKey(obj[currKey], key)
}
if (currKey === key) {
return obj[key]
}
}
}
const res = getObjectValueOfKey(obj, 'aa')
console.log('res', res)
but for some unknown to me reasons res is undefined.
you were pretty close, the recursion was good but the problem is related with what all the paths in your code doesn't return, actually the only path that return is when you found the key in the first level of the object, after you made the recursion and found the result but nothing was returned, the code reach the end the of function and javascript return by default undefined if any return is not found, to solve the problem a decide to store the results found in the recursion in a variable and return that value at the end of the function.
const getObjectValueOfKey = (obj, key, found) => {
const keys = Object.keys(obj);
let result = undefined;
result ||= found;
for (let currKey of keys) {
if (typeof obj[currKey] === "object") {
result ||= getObjectValueOfKey(obj[currKey], key, result);
}
if (currKey === key) {
return obj[key];
}
}
return result;
};
const res = getObjectValueOfKey(obj, "aa", undefined);
console.log("res", res);

Could you have multiple ways to run a function in Javascript?

I am setting up a database package in Node.js and would like to not have separate functions for writing to the database like this:
write(key, val) and write({key: val, key2: val2}). I've seen other solutions on Stack Overflow and other websites and would like to have the simplest solution so my function would "know" whether it was a key, val pair or a JSON object. For example:
if (argtype == "kvp") { // key val pair
databaseJSON[key] = val;
flushToDB(databaseJSON);
} else {
let j = databaseJSON;
for (let i in Object.values(obj)) j[Object.keys(obj)[i]] = Object.values(obj)[i];
flushToDB(databaseJSON);
}
Thank you!
If two arguments are passed, just assign the val to the key property of the database, otherwise Object.assign the one argument to the database, to put all of its properties and values from the passed object to the database:
function write(key, val) {
if (val !== undefined) {
database[key] = val;
} else {
Object.assign(database, key);
}
flushToDB(database)
}
const database = {};
const flushToDB = db => console.log('flushing');
function write(key, val) {
if (val !== undefined) {
database[key] = val;
} else {
Object.assign(database, key);
}
flushToDB(database)
}
write('key', 'val');
console.log('db:', database);
write({ key2: 'val2', key3: 'val3' });
console.log('db:', database);
Because the database here is a plain object, not JSON (something in JSON format is a string, which is not the case here), better to call it database rather than databaseJSON. (See There's no such thing as a "JSON Object")
You can use this.
function foo() {
if (arguments.length == 1) {
// use your object code on arguments[0]
let j = databaseJSON;
let obj = arguments[0]
for (let i in Object.values(obj))
j[Object.keys(obj)[i]] = Object.values(obj)[i];
flushToDB(databaseJSON);
} else {
//user arguments[0] and arguments[1] for you key value
databaseJSON[arguments[0]] = arguments[1];
flushToDB(databaseJSON);
}
}
You can also throw error if arguments.length == 0

Proxy HTMLElement

I wanted to check what does a library do with video element I pass to it so I naively did this:
cosnt videoElement = new Proxy(document.querySelector('video'), {
get(target, key) {
const name = typeof key === 'symbol'? key.toString() : key;
console.info(`Accessing video.${ name }`)
return target[key];
}
});
But I got an error:
TypeError: Failed to execute 'contains' on 'Node': parameter 1 is not of type 'Node'.
Is there a way to make this work?
EDIT: I have gained some knowledge, with it I updated my proxy as fallow:
cosnt videoElement = document.querySelector('video');
cosnt proxyElement = new Proxy(videoElement , {
get(target, key) {
if (key == '___element___') {
return video;
}
const name = typeof key === 'symbol'? key.toString() : key;
console.info(`Accessing video.${ name }`);
const value = video[key];
if (value instanceof Function) {
return video[key].bind(video);
} else {
return video[key];
}
},
set(target, key, value) {
const name = typeof key === 'symbol'? key.toString() : key;
console.info(`Writing video.${ name } = ${ value }`);
video[key] = value;
return true;
}
});
It is meant for debugging, so I edited the compiled code and replaced all DOM manipulation references with element.___element___.
Next I found out that there seem to be problems with calling functions trough proxy, so I added the .bind(video) part.
And finally setting values was throwing. So I had to replace target with direct video reference (as matter of fact, I replaced all target references with video, just to be sure), that made it work... I am not sure why or how, but it did.
Questions are:
Is this really how it is supposed to be? (the document.body.contains(myProxyElement) part)
Setting values to video element throwing when setting inside proxy seemed weird, is it a bug? (3rd point, kinda second too, I suppose it is connected)
Bonus: playground
const video = document.querySelector('video');
const proxy = new Proxy(video, {
get(target, key) {
console.log(`Getting video.${typeof key === 'symbol'? key.toString() : key}`);
const value = video[key];
if (value instanceof Function) {
return value.bind(video);
} else {
return value;
}
},
set(target, key, value) {
console.log(`Setting video.${typeof key === 'symbol'? key.toString() : key} =`, value);
video[key] = value;
return true;
}
});
proxy.muted = true;
proxy.play();
proxy.controls = true;
try {
console.log(document.body.contains(proxy));
} catch (e) {
console.error('DOM operation failed', e);
}
video { max-width: 100%; }
<video src="//vjs.zencdn.net/v/oceans.mp4">
As already mentioned in the comments, the Proxy object will not get automatically casted into Node when calling document.body.contains(proxy).
Therefore, you can i.e. set a specific key that will return the proxy's target:
const video = document.querySelector('video');
const proxy = new Proxy(video, {
get(target, key) {
const value = video[key];
if (value instanceof Function) {
return value.bind(video);
} else if (key == 'target') {
return target;
} else {
return value;
}
},
set(target, key, value) {
target[key] = value;
return true;
}
});
And then you can do:
console.log(document.body.contains(proxy.target));
Edit:
Is this really how it is supposed to be? (the
document.body.contains(myProxyElement) part):
Yes, I do not see any other way how to do this.
Setting values to video element throwing when setting inside proxy
seemed weird, is it a bug? (3rd point, kinda second too, I suppose it
is connected):
Since I am not able to reproduce this issue, it is hard to say. Maybe try to replace new Proxy(video, ... with new Proxy({}, ... and then set the video as proxy.video = video (you will have to also update the logic in the get and set) and see how that behaves?

Recursive nested property creation in JavaScript

I am trying to recursively build an object with a tree of properties based on a MongoDB-ish selector "top.middle.bottom". There are some underscorejs helpers as well:
function setNestedPropertyValue(obj, fields, val) {
if (fields.indexOf(".") === -1) {
// On last property, set the value
obj[fields] = val;
return obj; // Recurse back up
} else {
var oneLevelLess = _.first(fields.split("."));
var remainingLevels = _.rest(fields.split(".")).join(".");
// There are more property levels remaining, set a sub with a recursive call
obj[oneLevelLess] = setNestedPropertyValue( {}, remainingLevels, val);
}
}
setNestedPropertyValue({}, "grandpaprop.papaprop.babyprop", 1);
Desired:
{
grandpaprop: {
papaprop: {
babyprop: 1
}
}
}
Outcome:
undefined
Helps and hints would be appreciated.
Instead of recursion I would choose for an iterative solution:
function setNestedPropertyValue(obj, fields, val)
{
fields = fields.split('.');
var cur = obj,
last = fields.pop();
fields.forEach(function(field) {
cur[field] = {};
cur = cur[field];
});
cur[last] = val;
return obj;
}
setNestedPropertyValue({}, "grandpaprop.papaprop.babyprop", 1);
EDIT
And here is another version thanks to the suggestions by Scott Sauyet:
function setPath(obj, [first, ...rest], val) {
if (rest.length == 0) {
return {...obj, [first]: val}
}
let nestedObj = obj[first] || {};
return {...obj, [first]: setPath(nestedObj, rest, val)};
}
function setNestedPropertyValue(obj, field, val) {
return setPath(obj, field.split('.'), val);
}
// example
let test_obj = {};
test_obj = setNestedPropertyValue(test_obj, "foo.bar.baz", 1);
test_obj = setNestedPropertyValue(test_obj, "foo.bar.baz1", 1);
// will output {"foo":{"bar":{"baz":1,"baz1":1}}}, while in the original version only "baz1" will be set
console.log(JSON.stringify(test_obj));
It's plain javascript
It only appends properties and will not override a top level object
setNestedPropertyValue() does not mutate the passed object (although keep in mind it only returns a shallow copy of the object, so some properties may be shared references between the original object and the new one)
I know this is old, but I needed exactly that kind of function and wasn't happy with the implementation, so here is my version:
function setNestedPropertyValue(obj, field, val) {
if (field.indexOf(".") === -1) {
obj[field] = val;
} else {
let fields = field.split(".");
let topLevelField = fields.shift();
let remainingFields = fields.join(".");
if (obj[topLevelField] == null) {
obj[topLevelField] = {};
}
setNestedPropertyValue(obj[topLevelField], remainingFields, val);
}
}
// example
let test_obj = {};
setNestedPropertyValue(test_obj, "foo.bar.baz", 1);
setNestedPropertyValue(test_obj, "foo.bar.baz1", 1);
// will output {"foo":{"bar":{"baz":1,"baz1":1}}}, while in the original version only "baz1" will be set
console.log(JSON.stringify(test_obj));
It's plain javascript
It only appends properties and will not override a top level object
setNestedPropertyValue() does not return the object so it is clear that it mutates the passed object
As mentioned by Jack in the question, I was not returning my object in the last line in the else statement. By adding this, it is now working:
obj[oneLevelLess] = setNestedPropertyValue( {}, remainingLevels, val);
return obj; // Add this line
}

Ember model to json

I am looking for an efficient way to translate my Ember object to a json string, to use it in a websocket message below
/*
* Model
*/
App.node = Ember.Object.extend({
name: 'theName',
type: 'theType',
value: 'theValue',
})
The websocket method:
App.io.emit('node', {node: hash});
hash should be the json representation of the node. {name: thename, type: theType, ..}
There must be a fast onliner to do this.. I dont want to do it manualy since i have many attributes and they are likely to change..
As stated you can take inspiration from the ember-runtime/lib/core.js#inspect function to get the keys of an object, see http://jsfiddle.net/pangratz666/UUusD/
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, ret = [];
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
} // ignore useless items
if (Ember.typeOf(v) === 'function') {
continue;
}
ret.push(key);
}
}
return this.getProperties.apply(this, ret);
}
});
Note, since commit 1124005 - which is available in ember-latest.js and in the next release - you can pass the ret array directly to getProperties, so the return statement of the getJson function looks like this:
return this.getProperties(ret);
You can get a plain JS object (or hash) from an Ember.Object instance by calling getProperties() with a list of keys.
If you want it as a string, you can use JSON.stringify().
For example:
var obj = Ember.Object.create({firstName: 'Erik', lastName: 'Bryn', login: 'ebryn'}),
hash = obj.getProperties('firstName', 'lastName'), // => {firstName: 'Erik', lastName: 'Bryn'}
stringHash = JSON.stringify(hash); // => '{"firstName": "Erik", "lastName": "Bryn"}'
I have also been struggling with this. As Mirko says, if you pass the ember object to JSON.stringify you will get circular reference error. However if you store the object inside one property and use stringify on that object, it works, even nested subproperties.
var node = Ember.Object.create({
data: {
name: 'theName',
type: 'theType',
value: 'theValue'
}
});
console.log(JSON.stringify(node.get('data')));
However, this only works in Chrome, Safari and Firefox. In IE8 I get a stack overflow so this isn't a viable solution.
I have resorted to creating JSON schemas over my object models and written a recursive function to iterate over the objects using the properties in the schemas and then construct pure Javascript objects which I can then stringify and send to my server. I also use the schemas for validation so this solution works pretty well for me but if you have very large and dynamic data models this isn't possible. I'm also interested in simpler ways to accomplish this.
I modifed #pangratz solution slightly to make it handle nested hierarchies of Jsonables:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {};
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
App.io.emit('node', {node: node.toJSON()});
Or if you have an ID property and want to include it:
App.io.emit('node', {node: node.toJSON({includeId: true})});
Will this work for you?
var json = JSON.stringify( Ember.getMeta( App.node, 'values') );
The false is optional, but would be more performant if you do not intend to modify any of the properties, which is the case according to your question. This works for me, but I am wary that Ember.meta is a private method and may work differently or not even be available in future releases. (Although, it isn't immediately clear to me if Ember.getMeta() is private). You can view it in its latest source form here:
https://github.com/emberjs/ember.js/blob/master/packages/ember-metal/lib/utils.js
The values property contains only 'normal' properties. You can collect any cached, computed properties from Ember.meta( App.node, false ).cached. So, provided you use jQuery with your build, you can easily merge these two objects like so:
$.extend( {}, Ember.getMeta(App.node, 'values'), Ember.getMeta(App.node, 'cache') );
Sadly, I haven't found a way to get sub-structures like array properties in this manner.
I've written an extensive article on how you can convert ember models into native objects or JSON which may help you or others :)
http://pixelchild.com.au/post/44614363941/how-to-convert-ember-objects-to-json
http://byronsalau.com/blog/convert-ember-objects-to-json/
I modified #Kevin-pauli solution to make it works with arrays as well:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {}, inspectArray = function (aSome) {
if (Ember.typeof(aSome) === 'array') {
return aSome.map(inspectArray);
}
if (Jsonable.detect(aSome)) {
return aSome.getJson();
}
return aSome;
};
for (var key in this) {
if (this.hasOwnProperty(key)) {
v = this[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (Ember.typeOf(v) === 'array') {
v = v.map(inspectArray);
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
I also made some further modification to get the best of both worlds. With the following version I check if the Jsonable object has a specific property that informs me on which of its properties should be serialized:
App.Jsonable = Ember.Mixin.create({
getJson: function() {
var v, json = {}, base, inspectArray = function (aSome) {
if (Ember.typeof(aSome) === 'array') {
return aSome.map(inspectArray);
}
if (Jsonable.detect(aSome)) {
return aSome.getJson();
}
return aSome;
};
if (!Ember.isNone(this.get('jsonProperties'))) {
// the object has a selective list of properties to inspect
base = this.getProperties(this.get('jsonProperties'));
} else {
// no list given: let's use all the properties
base = this;
}
for (var key in base) {
if (base.hasOwnProperty(key)) {
v = base[key];
if (v === 'toString') {
continue;
}
if (Ember.typeOf(v) === 'function') {
continue;
}
if (Ember.typeOf(v) === 'array') {
v = v.map(inspectArray);
}
if (App.Jsonable.detect(v))
v = v.getJson();
json[key] = v;
}
}
return json;
}
});
I am using this little tweak and I am happy with it. I hope it'll help others as well!
Thanks to #pangratz and #Kevin-Pauli for their solution!
Here I take #leo, #pangratz and #kevin-pauli solution a little step further. Now it iterates not only with arrays but also through has many relationships, it doesn't check if a value has the type Array but it calls the isArray function defined in Ember's API.
Coffeescript
App.Jsonable = Em.Mixin.create
getJson: ->
jsonValue = (attr) ->
return attr.map(jsonValue) if Em.isArray(attr)
return attr.getJson() if App.Jsonable.detect(attr)
attr
base =
if Em.isNone(#get('jsonProperties'))
# no list given: let's use all the properties
this
else
# the object has a selective list of properties to inspect
#getProperties(#get('jsonProperties'))
hash = {}
for own key, value of base
continue if value is 'toString' or Em.typeOf(value) is 'function'
json[key] = jsonValue(value)
json
Javascript
var hasProp = {}.hasOwnProperty;
App.Jsonable = Em.Mixin.create({
getJson: function() {
var base, hash, hashValue, key, value;
jsonValue = function(attr) {
if (Em.isArray(attr)) {
return attr.map(jsonValue);
}
if (App.Jsonable.detect(attr)) {
return attr.getJson();
}
return attr;
};
base = Em.isNone(this.get('jsonProperties')) ? this : this.getProperties(this.get('jsonProperties'));
json = {};
for (key in base) {
if (!hasProp.call(base, key)) continue;
value = base[key];
if (value === 'toString' || Em.typeOf(value) === 'function') {
continue;
}
json[key] = jsonValue(value);
}
return json;
}
});
Ember Data Model's object counts with a toJSON method which optionally receives an plain object with includeId property used to convert an Ember Data Model into a JSON with the properties of the model.
https://api.emberjs.com/ember-data/2.10/classes/DS.Model/methods/toJSON?anchor=toJSON
You can use it as follows:
const objects = models.map((model) => model.toJSON({ includeId: true }));
Hope it helps. Enjoy!
I have:
fixed and simplified code
added circular reference prevention
added use of get of value
removed all of the default properties of an empty component
//Modified by Shimon Doodkin
//Based on answers of: #leo, #pangratz, #kevin-pauli, #Klaus
//http://stackoverflow.com/questions/8669340
App.Jsonable = Em.Mixin.create({
getJson : function (keysToSkip, visited) {
//getJson() called with no arguments,
// they are to pass on values during recursion.
if (!keysToSkip)
keysToSkip = Object.keys(Ember.Component.create());
if (!visited)
visited = [];
visited.push(this);
var getIsFunction;
var jsonValue = function (attr, key, obj) {
if (Em.isArray(attr))
return attr.map(jsonValue);
if (App.Jsonable.detect(attr))
return attr.getJson(keysToSkip, visited);
return getIsFunction?obj.get(key):attr;
};
var base;
if (!Em.isNone(this.get('jsonProperties')))
base = this.getProperties(this.get('jsonProperties'));
else
base = this;
getIsFunction=Em.typeOf(base.get) === 'function';
var json = {};
var hasProp = Object.prototype.hasOwnProperty;
for (var key in base) {
if (!hasProp.call(base, key) || keysToSkip.indexOf(key) != -1)
continue;
var value = base[key];
// there are usual circular references
// on keys: ownerView, controller, context === base
if ( value === base ||
value === 'toString' ||
Em.typeOf(value) === 'function')
continue;
// optional, works also without this,
// the rule above if value === base covers the usual case
if (visited.indexOf(value) != -1)
continue;
json[key] = jsonValue(value, key, base);
}
visited.pop();
return json;
}
});
/*
example:
DeliveryInfoInput = Ember.Object.extend(App.Jsonable,{
jsonProperties: ["title","value","name"], //Optionally specify properties for json
title:"",
value:"",
input:false,
textarea:false,
size:22,
rows:"",
name:"",
hint:""
})
*/
Ember.js appears to have a JSON library available. I hopped into a console (Firebug) on one the Todos example and the following worked for me:
hash = { test:4 }
JSON.stringify(hash)
So you should be able to just change your line to
App.io.emit('node', { node:JSON.stringify(hash) })

Categories