I've never used Proxy before, but I think it should be possible to "merge" an collection of objects into a single object.
It needs to remain "live" because the original fields will have value changes performed on them.
Ignore key collisions at this stage:
Given:
const fields = [{
name: 'hello',
value: 1
},{
name: 'goodbye',
value : 2
}];
Output:
const proxy = { hello:1 , goodbye :2 }
I definitely need to be able to iterate over the proxied object with a for in.
Have start a pen here, but haven't got very far: https://codepen.io/anon/pen/mMRaKw?editors=1111
Is it possible?
Here is a solution with the Proxy target as an empty object (if the Array is proxied, the for in will iterate numbered entries).
const fields = [{name: 'hello',value: 1}, { name: 'goodbye', value: 2}];
let handler = {
get: function(target, name) {
var f = fields.find(f => f.name === name);
return f && f.value;
},
ownKeys: function(target) {
return fields.map(f => f.name);
},
getOwnPropertyDescriptor: function(target, prop) {
return { configurable: true, enumerable: true };
}
};
let prox = new Proxy({}, handler);
// update original
fields[0].value=10;
// change reflected in proxy
console.log('proxy.hello',prox.hello);
for( let i in prox ){
console.log(i)
console.log(prox[i])
}
console.log(prox)
I think you are looking for something like:
const fields = [{name: 'hello',value: 1}, { name: 'goodbye', value: 2}];
let handler = {
get: function(target, name) {
if (name === 'flatten') {
return target.reduce((a, c) => {
a[c.name] = c.value;
return a
}, {});
} else {
return target[name];
}
},
set: function(target, prop, value) {
let obj = target.find(o => o.name === prop);
if(obj) {
obj.value = value;
return true;
}
return false;
}
};
let prox = new Proxy(fields, handler);
console.log('flat obj', JSON.stringify(prox.flatten))
// update original
fields[0].value=10;
// change reflected in proxy
console.log('flatten.hello',prox.flatten.hello);
// update proxy
prox.goodbye = 200;
// change reflected in original
console.log('original', fields[1].value)
Related
I'm fairly new to ReactJS and wrote this function I want to use to update an object in my state. It seems unable to use the "name" param to update my object and I don't really get why. I tried to code it in template literals as well.
const handleAccountingChange = (newValue, name, id) => {
const newState = selected.map((obj) => {
if (obj.id === id) {
return { ...obj, name: newValue };
}
return obj;
});
setSelected(newState);
};
I get no error in the browser console, but it doesn't update my state either. Any idea would be appreciated. I spent 2 hours on google but didn't find anything.
When you call obj.property = 'aaa' you set property to aaa.
What you try to do is update the property contained by the variable name, what you code does is update the property name.
To update a property from a variable you need to use :
const property = 'name'
obj[property] = 'aaa'
equivalente to :
obj.name == 'aaa'
This code solves your probleme :
const handleAccountingChange = (newValue, name, id) => {
// For the exemple I declare selected here
const selected = [ {id: 1, test: 'aaa'}, {id: 2, test: 'bbb'} ];
const newState = selected.map((obj) => {
if (obj.id === id) {
let myObj = {...obj};
myObj[name] = newValue;
return myObj;
}
return obj;
});
return newState; // instead ou can use setSelected(newState)
};
console.log(handleAccountingChange('ccc', 'test', 1));
const handleAccountingChange = (newValue, name, id) => {
const newState = selected.map((obj) => {
if (obj.id === id) {
return { obj[name]= newValue};
}
return obj;
});
setSelected({...newState});
}
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>());
In Firestore you can update fields in nested objects by a dot notation (https://firebase.google.com/docs/firestore/manage-data/add-data?authuser=0#update_fields_in_nested_objects). I wonder how to make that work in Typescript / Javascript.
For example the following object:
const user = {
id: 1
details: {
name: 'Max',
street: 'Examplestreet 38',
email: {
address: 'max#example.com',
verified: true
}
},
token: {
custom: 'safghhattgaggsa',
public: 'fsavvsadgga'
}
}
How can I update this object with the following changes:
details.email.verified = false;
token.custom = 'kka';
I already found that Lodash has a set function:
_.set(user, 'details.email.verified', false);
Disadvantage: I have to do this for every change. Is their already a method to update the object with an object (like firestore did)?
const newUser = ANYFUNCTION(user, {
'details.email.verified': false,
'token.custom' = 'kka'
});
// OUTPUT for newUser would be
{
id: 1
details: {
name: 'Max',
street: 'Examplestreet 38',
email: {
address: 'max#example.com',
verified: false
}
},
token: {
custom: 'kka',
public: 'fsavvsadgga'
}
}
Does anyone know an good solution for this? I already found more solutions if I only want to change one field (Dynamically set property of nested object), but no solution for more than one field with one method
I think you are stuck with using a function but you could write it yourself. No need for a lib:
function set(obj, path, value) {
let parts = path.split(".");
let last = parts.pop();
let lastObj = parts.reduce((acc, cur) => acc[cur], obj);
lastObj[last] = value;
}
set(user, 'details.email.verified', false);
if what you want to do is merge 2 objects then it is a bit trickier:
function forEach(target, fn) {
const keys = Object.keys(target);
let i = -1;
while (++i < keys.length) {
fn(target[keys[i]], keys[i]);
}
}
function setValues(obj, src) {
forEach(src, (value, key) => {
if (value !== null && typeof (value) === "object") {
setValues(obj[key], value);
} else {
obj[key] = value;
}
});
}
let obj1 = {foo: {bar: 1, boo: {zot: null}}};
let obj2 = {foo: {baz: 3, boo: {zot: 5}}};
setValues(obj1, obj2);
console.log(JSON.stringify(obj1));
One solution in combination with lodash _.set method could be:
function setObject(obj, paths) {
for (const p of Object.keys(paths)) {
obj = _.set(obj, p, paths[p]);
}
return obj;
}
I have this json object returned from an API that has a few quirks, and I'd like to normalize it so I can process the input the same for every response. These means getting rid of superfluous keys:
Response:
{
_links: {...},
_embedded: {
foo: [
{
id: 2,
_embedded: {
bar: []
}
}
]
}
}
So I'd like to remove all the _embedded keys and flatten it, like so:
{
_links: {...},
foo: [
{
id: 2,
bar: []
}
]
}
This is what I have at the moment, but it only works for the top level and I don't think it'll play well with arrays.
_.reduce(temp1, function(accumulator, value, key) {
if (key === '_embedded') {
return _.merge(accumulator, value);
}
return accumulator[key] = value;
}, {})
Loop in recursion on all of your keys, once you see a key which start with _
simply remove it.
Code:
var
// The keys we want to remove from the Object
KEYS_TO_REMOVE = ['_embedded'],
// The data which we will use
data = {
_links: {'a': 1},
_embedded: {
foo: [
{
id: 2,
_embedded: {
bar: []
}
},
{
id: 3,
_embedded: {
bar: [
{
id: 4,
_embedded: {
bar: []
}
}
]
}
}
]
}
};
/**
* Flatten the given object and remove the desired keys if needed
* #param obj
*/
function flattenObject(obj, flattenObj) {
var key;
// Check to see if we have flatten obj or not
flattenObj = flattenObj || {};
// Loop over all the object keys and process them
for (key in obj) {
// Check that we are running on the object key
if (obj.hasOwnProperty(key)) {
// Check to see if the current key is in the "black" list or not
if (KEYS_TO_REMOVE.indexOf(key) === -1) {
// Process the inner object without this key
flattenObj[key] = flattenObject(obj[key], flattenObj[key]);
} else {
flattenObject(obj[key], flattenObj);
}
}
}
return flattenObj;
}
console.log(flattenObject(data));
So, basically you already have almost all of the code you need. All we have to do is wrap it in a function so we can use recursion. You'll see we only add a check to see if it is an object, if it is, we already have a function that knows how to flatten that object, so we'll just call it again with the key that we need to flatten.
function flatten(temp1) { // Wrap in a function so we can use recursion
return _.reduce(temp1, function(accumulator, value, key) {
if (key === '_embedded') {
return _.merge(accumulator, value);
} else if (value !== null && typeof value === 'object') // Check if it's another object
return _.merge(accumulator, flatten(value)) // Call our function again
return accumulator[key] = value;
}, {})
}
I'll be able to test it in a bit, but this should be what you need.
Got it!
function unEmbed(data) {
return _.reduce(data, function(accumulator, value, key) {
const returnableValue = _.isObject(value) ? unEmbed(value) : value;
if (key === 'embedded') {
return _.merge(accumulator, returnableValue);
}
accumulator[key] = returnableValue;
return accumulator;
}, {});
}
Problem before I was returning return accumulator[key] = returnableValue, which worked out to be return returnableValue.
I'm trying to setup an object literal in a JavaScript script that has a key with multiple names. referring to the same object value i.e. something like these that I have already tried:
var holidays: {
"thanksgiving day", "thanksgiving", "t-day": {
someValue : "foo"
}
}
var holidays: {
["thanksgiving day", "thanksgiving", "t-day"]: {
someValue : "foo"
}
}
Is there a way I can accomplish this?
Another approach is to do some postprocessing
function expand(obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; ++i) {
var key = keys[i],
subkeys = key.split(/,\s?/),
target = obj[key];
delete obj[key];
subkeys.forEach(function(key) { obj[key] = target; })
}
return obj;
}
var holidays = expand({
"thanksgiving day, thanksgiving, t-day": {
someValue : "foo"
}
});
JSON does not offer such a feature, nor do Javascript object literals.
You might be able to make do with something like this:
holidays = {
thanksgiving: {foo: 'foo'},
groundhogDay: {foo: 'bar'},
aliases: {
'thanksgiving day': 'thanksgiving',
't-day': 'thanksgiving',
'Bill Murrays nightmare': 'groundhogDay'
}
}
and then you can check
holidays[name] || holidays[holidays.aliases[name]]
for your data.
It's not a wonderful solution. But it wouldn't be too difficult to write a little function that created this sort of object out of a representation like:
[
{
names: ['thanksgiving', 'thanksgiving day', 't-day'],
obj: {foo: 'foo'}
},
{
names: ['groundhogDay', 'Bill Murrays nightmare'],
obj: {foo: 'bar'}
},
]
if that would be easier to maintain.
Another solution, if you can afford RegExp execution, and ES6 Proxy:
let align = new Proxy({
'start|top|left': -1,
'middle|center': 0,
'end|bottom|right': 1,
}, {
get: function(target, property, receiver) {
for (let k in target)
if (new RegExp(k).test(property))
return target[k]
return null
}
})
align.start // -1
align.top // -1
align.left // -1
align.middle // 0
align.center // 0
align.end // 1
align.bottom // 1
align.right // 1
See MDN Proxy
2021 EDIT:
Another (cleaner?) solution using reduce & defineProperty :
const myDict = [
// list of pairs [value, keys],
// note that a key should appear only once
[-1, ['start', 'left', 'top']],
[0, ['center', 'middle']],
[1, ['end', 'right', 'bottom']],
].reduce((obj, [value, keys]) => {
for (const key of keys) {
Object.defineProperty(obj, key, { value })
}
return obj
}, {})
I guess you could do something like this:
var holidays = {
'thanksgiving day': {
foo: 'foo'
}
};
holidays.thanksgiving = holidays['t-day'] = holidays['thanksgiving day'];
If you see yourself doing this often or you have more values consider this pattern:
'thanksgiving, t-day, thanks, thank, thank u'.split(',').forEach(function(key) {
holidays[key] = holidays['thanksgiving day'];
});
A better approach would be to process your data beforehand instead of adding duplicates.
That should work as expected:
function getItem(_key) {
items = [{
item: 'a',
keys: ['xyz','foo']
},{
item: 'b',
keys: ['xwt','bar']
}];
_filtered = items.filter(function(item) {
return item.keys.indexOf(_key) != -1
}).map(function(item) {
return item.item;
});
return !!_filtered.length ? _filtered[0] : false;
}
With ES6 you could do it like this, but it's not ideal:
const holidays = {
"single": {
singleValue: "foo",
},
...([
"thanksgiving day", "thanksgiving", "t-day",
].reduce((a, v) => ({...a, [v]: {
someValue: "foo",
}}), {})),
"other": {
otherValue: "foo",
},
};
I still think the cleanest solution is probably:
let holidays = {
"t-day": {
someValue: "foo",
},
};
holidays["thanksgiving"] = holidays["t-day"];
holidays["thanksgiving day"] = holidays["t-day"];
Now this may be overkill for you, but here's a generic function that will create an object with "multiple keys." What it actually does is have one real property with the actual value, and then defines getters and setters to forward operations from the virtual keys to the actual property.
function multiKey(keyGroups) {
let obj = {};
let props = {};
for (let keyGroup of keyGroups) {
let masterKey = keyGroup[0];
let prop = {
configurable: true,
enumerable: false,
get() {
return obj[masterKey];
},
set(value) {
obj[masterKey] = value;
}
};
obj[masterKey] = undefined;
for (let i = 1; i < keyGroup.length; ++i) {
if (keyGroup.hasOwnProperty(i)) {
props[keyGroup[i]] = prop;
}
}
}
return Object.defineProperties(obj, props);
}
This is less sketchy than you would expect, has basically no performance penalty once the object is created, and behaves nicely with enumeration (for...in loops) and membership testing (in operator). Here's some example usage:
let test = multiKey([
['north', 'up'],
['south', 'down'],
['east', 'left'],
['west', 'right']
]);
test.north = 42;
test.down = 123;
test.up; // returns 42
test.south; // returns 123
let count = 0;
for (let key in test) {
count += 1;
}
count === 4; // true; only unique (un-linked) properties are looped over
Taken from my Gist, which you may fork.
Same reponse (ES6 Proxy, RegExp), but in a shorter way (and significantly less legible)
let align = new Proxy({
'start|top|left': -1,
'middle|center': 0,
'end|bottom|right': 1,
}, { get: (t, p) => Object.keys(t).reduce((r, v) => r !== undefined ? r : (new RegExp(v).test(p) ? t[v] : undefined), undefined) })
align.start // -1
align.top // -1
align.left // -1
align.middle // 0
align.center // 0
align.end // 1
align.bottom // 1
align.right // 1
//create some objects(!) you want to have aliases for..like tags
var {learn,image,programming} =
["learn", "image", "programming"].map(tag=>({toString:()=>tag }));
//create arbitrary many aliases using a Map
var alias = new Map();
alias.set("photo", image);
alias.set("pic", image);
alias.set("learning", learn);
alias.set("coding", programming);
//best put the original tagNames in here too..
//pretty easy huh?
// returns the image object
alias.get("pic");
// ;)
here is a way you can initialize an object with several keys sharing the same value
var holidays = {
...["thanksgiving day", "thanksgiving", "t-day"].reduce((acc, key) => ({ ...acc, [key]: 'foo' }), {})
}
although I would personally think it was more clear if it was written out
Object.fromEntries produces some fairly readable and concise code:
var holidays = Object.fromEntries(
["thanksgiving day", "thanksgiving", "t-day"].map(k => [k, "foo"]));
The spread syntax can be used to include this alongside other key/value pairs:
var holidaysAndMore = {
"A": "a",
...Object.fromEntries(
["thanksgiving day", "thanksgiving", "t-day"].map(k => [k, "foo"])),
"B": "b"
};