I have a complex object in typescript that i want to be able to change with a single function (I know how hacky this sounds, but its a hobby project). The function takes a value and a path as a rest-parameter. The path can be of any length.
I want to change the property, but so far I've only come up with solutions that lose the reference to the original object, and as such is not a working solution.
I've tried using both whiles and for loops to iterate over the array and "zooming" in on the property. In each case, they lost the reference to the original object and thus didn't mutate it.
I've tried accessing the object directly, with a known length, and that works but its hardcoded to a length and as such isn't a good solution either. In a desperate case, i could make a function like this for each size I'm expecting (it's somewhat limited) but that hurts my pride.
Example
character: Character = {
characteristics: {
0: {
initial: 30,
advances: 15
}
1...
}
}
this.updateProperty(35, characteristics, 0, initial) //Change characteristics.0.initial to be 35 instead of 30
With a for/while loop:
updateProperty(value: string|number,...path: string[]) {
let scope: Object = this.character;
for(let p of path) {
scope = scope[p];
}
scope = value;
console.log(scope);
console.log(this.character);
}
the scope is updated correctly, but the character is not changed
With direct access (Here of length 3)
updateProperty(value: string|number,...path: string[]) {
this.character[path[0]][path[1]][path[2]] = value;
}
Here its updated correctly, but its no longer taking in any length as it will crash if its longer than 3 and break it if shorter than 3
Attempt to access it directly with an array
updateProperty(value: string|number,...path: string[]) {
this.character[path] = value;
}
Gives
ERROR in src/app/character.service.ts(27,20): error TS2538: Type 'string[]' cannot be used as an index type.
Considering your direct access works fine for you, you can try something like:
let character = {
characteristics: {
0 : {
initial: 30,
advances: 15
}
}
}
function updateProperty(value,...path) {
let finalOb, i;
for (i = 0; i < path.length; i ++) {
let tempOb
if (finalOb) {
tempOb = finalOb[path[i]];
} else {
tempOb = character[path[i]];
}
if (tempOb instanceof Array || typeof tempOb !== 'object') {
break;
} else {
finalOb = tempOb;
}
}
finalOb[path[i]] = value;
}
updateProperty(35, 'characteristics', '0', 'initial')
console.log(character)
updateProperty(value: string|number,...path: string[]) {
let scope: Object = this.character;
for(let p of path) {
scope = scope[p];
}
scope = value;
console.log(scope);
console.log(this.character);
this.character = scope; //?
}
Related
I am trying to delete an item from an object by passing a key to the method. For example I want to delete a1, and to do so I pass a.a1 to the method. It then should delete a1 from the object leaving the rest of the object alone.
This is the structure of the object:
this.record = {
id: '',
expiration: 0,
data: {
a: {
a1: 'Cat'
}
}
}
I then call this method:
delete(key) {
let path = key.split('.')
let data = path.reduce((obj, key) => typeof obj == 'object' ? obj[key] : null, this.record.data)
if(data) delete data
}
Like this:
let inst = new MyClass()
inst.delete('a.a1')
This however gives me the following error:
delete data;
^^^^
SyntaxError: Delete of an unqualified identifier in strict mode.
I assume that data is a reference still at this point, or is it not?
Maybe reduce isn't the right method to use here. How can I delete the item from the object?
Using your example, the value of data at the point where it is checked for truthiness is Cat, the value of the property you're trying to delete. At this point, data is just a regular variable that's referencing a string and it's no longer in the context of inst.
Here's a solution I managed to get to work using the one from your OP as the basis:
let path = key.split('.')
let owningObject = path.slice(0, path.length - 1)
.reduce((obj, key) => typeof obj == 'object' ? obj[key] : null, this.record.data)
if (owningObject) delete owningObject[path[path.length - 1]]
The main difference between this and what you had is that reduce operates on a slice of the path segments, which does not include the final identifier: This ends up with owningObject being a reference to the a object. The reduce is really just navigating along the path up until the penultimate segment, which itself is used as the property name that gets deleted.
For an invalid path, it bails out either because of the if (owningObject) or because using delete on an unknown property is a no-op anyway.
The solution I came up with which I am not super fond of but works, is looping over the items which will allow me to do long keys like this
a.a1
a.a1.a1-1
a.a1.a1-1.sub
The function then looks like this
let record = {
data: {
a: {
a1: 'Cat',
a2: {
val: 'Dog'
}
}
}
}
function remove(key) {
let path = key.split('.')
let obj = record.data
for (let i = 0; i < path.length; i++) {
if (i + 1 == path.length && obj && obj[path[i]]) delete obj[path[i]]
else if(obj && obj[path[i]]) obj = obj[path[i]]
else obj = null
}
}
// Removes `a.a1`
remove('a.a1')
console.log(JSON.stringify(record))
// Removes `a.a2.val`
remove('a.a2.val')
console.log(JSON.stringify(record))
// Removes nothing since the path is invalid
remove('a.a2.val.asdf.fsdf')
console.log(JSON.stringify(record))
You can delete keys using [] references.
var foo = {
a: 1,
b: 2
};
var selector = "a";
delete foo[selector];
console.log(foo);
I'm not sure if this helps you but it might help someone googling to this question.
Here's another method which is very similar to the OP's own solution but uses Array.prototype.forEach to iterate over the path parts. I came to this result independently in my attempt to wrap this up as elegantly as possible.
function TestRecord(id, data) {
let record = {
id : id,
data : data
};
function removeDataProperty(key) {
let parent = record.data;
let parts = key.split('.');
let l = parts.length - 1;
parts.forEach((p, i) => {
if (i < l && parent[p]) parent = parent[p];
else if (i == l && parent[p]) delete parent[p];
else throw new Error('invalid key');
});
}
return {
record : record,
remove : function(key) {
try {
removeDataProperty(key);
} catch (e) {
console.warn(`key ${key} not found`);
}
}
}
}
let test = new TestRecord('TESTA', {
a : { a1 : '1', a2 : '2' },
b : { c : { d : '3' } }
});
test.remove('a'); // root level properties are supported
test.remove('b.c.d'); // deep nested properties are supported
test.remove('a.b.x'); // early exit loop and warn that property not found
console.log(test.record.data);
The usage of throw in this example is for the purpose of breaking out of the loop early if any part of the path is invalid since forEach does not support the break statement.
By the way, there is evidence that forEach is slower than a simple for loop but if the dataset is small enough or the readability vs efficiency tradeoff is acceptable for your use case then this may be a good alternative.
https://hackernoon.com/javascript-performance-test-for-vs-for-each-vs-map-reduce-filter-find-32c1113f19d7
This may not be the most elegant solution but you could achieve the desired result very quickly and easily by using eval().
function TestRecord(id) {
let record = {
id : id,
data : {
a : {
a1 : 'z',
a2 : 'y'
}
}
};
return {
record : record,
remove : function (key) {
if (!key.match(/^(?!.*\.$)(?:[a-z][a-z\d]*\.?)+$/i)) {
console.warn('invalid path');
return;
} else {
let cmd = 'delete this.record.data.' + key;
eval(cmd);
}
}
};
}
let t = new TestRecord('TESTA');
t.remove('a.a1');
console.log(t.record.data);
I have included a regular expression from another answer that validates the user input against the namespace format to prevent abuse/misuse.
By the way, I also used the method name remove instead of delete since delete is a reserved keyword in javascript.
Also, before the anti-eval downvotes start pouring in. From: https://humanwhocodes.com/blog/2013/06/25/eval-isnt-evil-just-misunderstood/ :
...you shouldn’t be afraid to use it when you have a case where eval()
makes sense. Try not using it first, but don’t let anyone scare you
into thinking your code is more fragile or less secure when eval() is
used appropriately.
I'm not promoting eval as the best way to manipulate objects (obviously a well defined object with a good interface would be the proper solution) but for the specific use-case of deleting a nested key from an object by passing a namespaced string as input, I don't think any amount of looping or parsing would be more efficient or succinct.
I'm trying to create an 'anonymous array' but it seems there is no such thing, is there some technique that would allow me to omit the array_of_objects: property from the G: object but keep everything else the same?
Since ECMAScript 2015 (or ES6), this:
{ array_of_objects }
is considered the same as
{ "array_of_objects": array_of_objects }
It's creating a property with that name. If you don't want this property, all you need to do is remove the curly braces:
return { cells: array_of_objects };
Here is the solution (if I understood your question correct)
setup: function() {
var cells = {}; // Better to use [], but nm
for(var x = 0; x< 121; x++) {
cells[x] = {id:x}
}
return { cells: cells }
}
I am basically trying to get this problem to work and have isolated the issue to line 21. I think the way I'm trying to access the object key is wrong. I am simply trying to say: if the object key in the new object exists in the original array, push the new value from the new object into the new array.
Edit to add code block
function valueReplace(array, obj) {
var replaced = [];
for (var i = 0; i < array.length; i += 1) {
var value = obj[array[i]];
if (array.indexOf(obj.i) !== -1) {
replaced.push(value);
} else {
replaced.push(array[i]);
}
}
return replaced;
}
You have a mixed up error report, but at the actual code, you try to access the object with the property i, obj.i, which not exists. Read more about property accessor.
For getting the wanted result, you might use the in operator for checking if a property in an object exists.
if (array[i] in obj) {
replaced.push(obj[array[i]]);
} else {
replaced.push(array[i]);
}
It looks like one of the issues you are having is trying to access a dynamic property with dot notation, which JS generally doesn't like. I reversed your logical if because IMO it makes more sense to see if the object has a property than get a property and then get the index of the array, but you could reverse it back to using index of by array.indexOf(obj[i]) !== -1
function valueReplace(array, obj) {
let replaced = [];
for (let i = 0, len = array.length; i < len; i++) {
if (obj.hasOwnProperty(array[i])) {
replaced.push(obj[array[i]]);
} else {
replaced.push(array[i]);
}
}
return replaced;
}
Because I generally like simplifying things here is this functionality rewritten in ES6 compatible code, using array.prototype.map. Don't use it for your homework, but if you want you can work it backwards into a standard function ;).
const valueReplace = (array, obj) => array.map(val => (obj.hasOwnProperty(val)) ? obj[val] : val);
I have an object which may or may not have nested objects and properties, and I want to access them using a string. Here's an example...
var obj = {
inside: {
value: 10,
furtherInside: {
value: 100
}
}
// may contain other objects, properties, etc.
};
function getObjProperty(str) {
return eval("obj." + str);
}
getObjProperty("inside.value"); // returns 10
getObjProperty("inside.furtherInside.value"); // returns 100
...But I'd like a solution that doesn't use eval.
How can this be done without using eval? I'm looking for the best/optimal/fastest solution.
How about something like
function getObjectProperty(obj, str) {
var props = str.split('.')
var result = obj;
for(var i = 0; i < props.length; i++)
result = result[props[i]];
return result;
}
This code assumes your strings are always valid and the object passed into getObjectProperty has properties that nest to the level you target, but it avoids eval. You could make it more robust with checks for undefined, but that may be overkill for what you need.
Test code, using your example:
var a = {
inside: {
value: 10,
furtherInside: {
value: 100
}
}
// may contain other objects, properties, etc.
};
console.log(getObjProperty(a, "inside.value")); // prints 10
console.log(getObjProperty(a, "inside.furtherInside.value")); // prints 100
You can use the brackets notation:
var obj = {
inside: {
value: 10,
furtherInside: {
value: 100
}
}
// may contain other objects, properties, etc.
};
alert(obj['inside']['furtherInside']['value']);
Then you may even use string properties like "my property":
var obj = {
"my property": 10
};
obj["my property"];
EDIT:
This is an approach (using brackets notation) to what you are asking for:
String.prototype.getVal = function(elem) {
var segments = this.split('.');
for (var i = 0; i < segments.length; i++) {
elem = elem[segments[i]];
}
return elem;
}
var obj = {
inside: {
value: 10,
furtherInside: {
value: 100
}
}
// may contain other objects, properties, etc.
};
console.log("inside.furtherInside.value".getVal(obj));
console.log("inside.value".getVal(obj));
http://jsfiddle.net/luismartin/kphtqd54
Since this method getVal() is being assigned to the String prototype, you may use it anywhere, and I think the implementation is pretty neat and fast. I hope this approach also helps getting rid of the negative vote :/
This is what I came up with, using some recursiveness...
function getObjProperty(obj, props) {
if (typeof props === 'string') {
if (props.indexOf('.') == -1) {
return obj[props];
} else {
props = props.split('.');
}
}
if (props.length == 1) {
return obj[props[0]];
} else if (props.length > 1) {
var top = props.shift();
return getObjProperty(obj[top], props);
} else {
return obj;
}
}
http://jsfiddle.net/0em2f6k6/
...But it's not as fast as a simple for-loop. http://jsperf.com/0em2f6k6
Although not vanilla JavaScript, another possibility is to use lodash's _.get function: https://lodash.com/docs#get.
_.get(obj, "inside.furtherInside.value");
It essentially does the same as #analytalica 's solution, except using a while loop (see the baseGet function in the lodash code), but it also allows strings or arrays (using the toPath function), and allows you to include a default.
I'm building an application which involves the creation of an array of objects, similar to this:
var foo = [{
'foo' : 'foo1'
},
{
'foo' : 'foo2'
},
{
'foo' : 'foo3'
}];
there's then an HTML form where the user fills in the values for new objects. When the form is submitted the new values are pushed to the array. what I want is an if/else statement which checks if the new object already exists in the array.
So something like:
document.getElementById('form').addEventListener('submit',function(){
var newObject = {'foo' : input value goes here }
if (//Checks that newObject doesn't already exist in the array) {
foo.push(newObject)
}
else {
//do nothing
}
});
It's also probably worth noting that I'm using Angular
You can use this approach:
You need:
Understand how to compare 2 objects.
Do it in cycle.
How to compare 2 objects.
One of the ways is:
JSON.stringify(obj1) === JSON.stringify(obj2)
Note, that comparing ojbects this way is not good:
Serializing objects merely to compare is terribly expensive and not
guaranteed to be reliable
As cookie monster mentioned in comments to this post.
I just suggested it, to achieve what you want. You can find better variant. You can find some beautiful answers here.
How to do it in cycle :D
In your case it will be:
function checkIfObjectExists(array, newObject) {
var i = 0;
for(i = 0; i < array.length; i++ ) {
var object = array[i];
if(JSON.stringify(object) === JSON.stringify(newObject))
{
return true;
}
}
return false;
}
Also, I added function, so you can use it in your code.
Now add this to your code:
if (checkIfObjectExists(foo, newObject)) {
// objects exists, do nothing
}
else {
foo.push(newObject);
}
DEMO
You'd have to loop through the foo-array and check for any duplicates.
document.getElementById('form').addEventListener('submit',function(){
var newObject = {'foo' : input value goes here }
if (!isInArray(foo, newObject, 'foo')) {
foo.push(newObject)
}
});
function isInArray(arr, newObj, type) {
var i, tempObj, result = false;
for (i = 0; i < arr.length; i += 1) {
tempObj = arr[i];
if (tempObj[type] === newObj[type]) {
result = true;
}
}
return result;
}
It's easier and faster if your array doesn't contain objects. Then you simply can make the if-clause to be:
document.getElementById('form').addEventListener('submit',function(){
var newString = "foo bar";
if (foo.indexOf(newString) === -1) {
foo.push(newString);
}
});