Better way to update an object's value at a variable depth

Multi tool use
Multi tool use


Better way to update an object's value at a variable depth



I am working on some software that reads/writes information in localStorage using a handler. You can find a working example here: http://jsbin.com/wifucugoko/edit?js,console


localStorage



My problem is with the segment of code below (focusing on the switch statement):


_t.set = function(path, value) { // Update a single value or object
if (~path.indexOf(".")) {
let o = path.split(".")[0],
p = this.get(o),
q = path.split(".").slice(1);
switch (q.length) {
// There has to be a better way to do this...
case 1:
p[q[0]] = value;
break;
case 2:
p[q[0]][q[1]] = value;
break;
case 3:
p[q[0]][q[1]][q[2]] = value;
break;
case 4:
p[q[0]][q[1]][q[2]][q[3]] = value;
break;
case 5:
p[q[0]][q[1]][q[2]][q[3]][q[4]] = value;
break;
case 6:
p[q[0]][q[1]][q[2]][q[3]][q[4]][q[5]] = value;
break;
default:
return "error";
}
b.setItem(o, JSON.stringify(p));
return p;
} else {
b.setItem(path, JSON.stringify(value));
return this.get(path);
}
};



I am not going to be the only one using this codebase, and I am trying to make it easy for others to update any value that could be placed in localStorage. Right now you can update a value by using something like local.set('item.subitem.proeprty', 'value') Though the code above does that, it's ugly and doesn't scale.


localStorage


local.set('item.subitem.proeprty', 'value')



How can this method be improved to (1) update a property nested at any depth automatically, instead of writing an infinitely-long switch statement, and (2) not lace a parent object with [object Object] after a value is updated?


[object Object]



This question has nothing to do with my use of localStorage. I originally posted this question in code review, which requires a working contextual example. They closed this question immediately, since part of my problem is the code I provided doesn't work once you start dealing with updating a value nested more than six objects deep. Though I could have continued my switch statement indefinitely, that's exactly what I'm trying to avoid.


localStorage



With the three examples provided you'll see that setting a value in one place doesn't remove values in other places:


local.set('user.session.timeout', false);
local.set('user.name', {first:'john', last:'doe', mi:'c'});
local.set('user.PIN', 8675309);



All these values, though set at different times, only UPDATE or create a value, they do NOT clear any pre-existing values elsewhere.





lodash has a function called set, that might be what your after -> lodash.com/docs/4.17.10#set
– Keith
Jun 22 at 14:42






Something I should mention is the owner of the project wants to keep things vanilla. Our last developer burned some bridges stuffing an insane amount of plugins into the site we're now replacing. But I'll take a peek at the source code for lodash :)
– invot
Jun 22 at 14:45





you could change the object, and then you're going to save the object in localStorage
– JackNavaRow
Jun 22 at 14:47





peek at the source code for lodash,.. you could certainly do that.. I can understand the code bloat issues, I think the developers of lodash understand this, and is the reason why you can pick and choose the bits you only need. Assuming your using a build system (ps. you should be), you could do require("lodash/set")
– Keith
Jun 22 at 14:51


peek at the source code for lodash


require("lodash/set")





What you seem to be asking currently is to traverse your object from a dot notation string. Then it is a duplicate of stackoverflow.com/questions/6393943/… But that sounds like a terrible design pattern and I don't see why you can't store your object directly as a JSON. (deserialize once at page load and serialize only when really required (e.g in unload might be enough)
– Kaiido
Jun 25 at 14:23





6 Answers
6



As for me the minimal optimization would be following:


if (~path.indexOf(".")) {
let o = path.split(".")[0],
p = this.get(o),
q = path.split(".").slice(1),
dist = p;
q.forEach(function(item, index) {
if (index < q.length - 1) {
dist = dist[item];
} else {
dist[item] = value;
}
});
b.setItem(o, JSON.stringify(p));
return p;
} else {



changed parts:



You could try something like this, if the path does not exists, the value is null:




function retreiveValueFromObject(path, object) {

var pathList = path.split(".");

var currentValue = object;
var brokeEarly = false;

for (var i = 0; i < pathList.length; i++) {

if (currentValue[pathList[i]]) {
currentValue = currentValue[pathList[i]];
} else {
brokeEarly = true;
break;
}
}

return {
value: currentValue,
brokeEarly: brokeEarly
};

}

function setValueInObject(path, value, object) {

var nestedObj = retreiveValueFromObject(path, object).value;

var pathList = path.split(".");
var finalKey = pathList[pathList.length - 1];
nestedObj[finalKey] = value;

}

var someObject = {
a: {
c: {
d: "value"
},
z: "c"
},
b: {
f: {
x: "world"
},
g: "hello"
},
};

console.log(retreiveValueFromObject("b.f.x", someObject));

setValueInObject("b.f.y", "newValue", someObject);

console.log(someObject);





I don't understand what your code does. I can't get it to update a value. jsbin.com/qodogibibe/edit?js,console
– invot
Jun 29 at 14:49



What you are looking for is a little bit of recursion, I just implemented the update method.


let localStorageHandler = function() {
let b = window.localStorage,
_t = this;
_t.get = function(a) {
try {
return JSON.parse(b.getItem(a))
} catch (c) {
return b.getItem(a)
}
};

function descendAndUpdate(obj, path, value) {
let current = path[0],
remainingPath = path.slice(1);
// found and update
if (obj.hasOwnProperty(current) && remainingPath.length === 0) {
obj[current] = value;
// found but still not there
} else if (obj.hasOwnProperty(current)) {
return descendAndUpdate(obj[current], remainingPath, value);
}
// if you want do add new properties use:
// obj[current] = value;
// in the else clause
else {
throw('can not update unknown property');
}
}

_t.set = function(path, value) { // Update a single value or object
if (~path.indexOf(".")) {
let o = path.split(".")[0],
p = this.get(o),
q = path.split(".").slice(1);
descendAndUpdate(p, q, value);
console.log(p);
b.setItem(o, JSON.stringify(p));
return p;
} else {
b.setItem(path, JSON.stringify(value));
return this.get(path);
}
};
_t.remove = function(a) { // removes a single object from localstorage
let c = !1;
a = "number" === typeof a ? this.key(a) : a;
a in b && (c = !0, b.removeItem(a));
return c
};
};
let local = new localStorageHandler();


// Create user and session info if it doesn't exist
let blankUser = new Object({
alias: '',
dob: '',
PIN: '',
level: 0,
name: {
first: '',
last: '',
mi:'',
},
session: {
token: '',
timeout: true,
lastChange: Date.now()
}
});

local.remove('user');

// Loads user data into localstorage
if (!local.get('user')) {
local.set('user', blankUser);
}

local.set('user.session.timeout', false);
local.set('user.name', {first:'john', last:'doe', mi:'c'});
local.set('user.PIN', 8675309);

// new property
// local.set('user.sunshine', { 'like': 'always' });

console.log(local.get('user'));



A friend of mine would always prefer stacks over recursion, which would be a second option. Anyway I agree with many of the comments here. You already know your domain model. Unless you have a very good reason for this approach spend more time on serializing and unserializing those objects in the database. I have the impression you would be able to work with your data in a more natural way because the aspect of updating fields in a database would be abstracted away.



I am working on a similar project at the moment. What I am doing is storing the data in something I called a WordMatrix (https://github.com/SamM/Rephrase/blob/master/WordMatrix.js), maybe you could use something like it in your solution.



My project is a WIP but the next step is literally to add support for localStorage. The project itself is a database editor that works with key => value stores. You can view the prototype for it here: (https://samm.github.io/Rephrase/editor.html)



Once I have implemented the localStorage aspect I will update this post.



Your topic reminds me one recent another topic.



Trying to enhance the answer I provided, I propose you these functions:




// Function to get a nested element:
function obj_get_path(obj, path) {
return path.split('.').reduce((accu, val) => accu[val] || 'Not found', obj);
}

// Function to set a nested element:
function obj_set_path(obj, path, value) {
var result = obj;
var paths = path.split('.');
var len = paths.length;
for (var i = 0; i < len - 1; i++) {
result = result[paths[i]] || {};
}
result[paths[len - 1]] = value;
return obj;
}

// Example object
var obj = {
name0: 'A name',
level0: {
name1: 'An other name',
level1: {
level2: {
name3: 'Name to be changed',
text3: 'Some other text'
}
}
}
}

// Use of the function
obj = obj_set_path(obj, 'level0.level1.level2.name3', 'Takit Isy');
obj = obj_set_path(obj, 'level0.level1.level2.new3', 'I’m a new element!');
var obj_level2 = obj_get_path(obj, 'level0.level1.level2');

// Consoling
console.log('Consoling of obj_level2:n', obj_level2);
console.log('nConsoling of full obj:n', obj); // To see that the object is correct







We could also adapt the 2nd function in my above snippet so that it works for both get and set, depending of if "value" is set:




// We could also adapt the second function for both uses:
function obj_path(obj, path, value = null) {
var result = obj;
var paths = path.split('.');
var len = paths.length;
for (var i = 0; i < len - 1; i++) {
result = result[paths[i]] || {};
}
// Return result if there is no set value
if (value === null) return result[paths[len - 1]];
// Set value and return
result[paths[len - 1]] = value;
return obj;

}

// Example object
var obj = {
name0: 'A name',
level0: {
name1: 'An other name',
level1: {
level2: {
name3: 'Name to be changed',
text3: 'Some other text'
}
}
}
}

// Use of the function
obj = obj_path(obj, 'level0.level1.level2.name3', 'Takit Isy');
obj = obj_path(obj, 'level0.level1.level2.new3', 'I’m a new element!');
var obj_level2 = obj_path(obj, 'level0.level1.level2');

// Consoling
console.log('Consoling of obj_level2:n', obj_level2);
console.log('nConsoling of full obj:n', obj); // To see that the object is correct



Hope it helps.



How about:




function parse(str) {
var arr = str.split('.');
return function(obj) {
return arr.reduce((o, i) => o[i], obj);
}
}


let foo = {
a: {
b: {
c: {
bar: 0
}
}
}

}

let c = parse('a.b.c')(foo);
console.log(c.bar);
c['bar'] = 1;
console.log(foo);






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

4zf0KC8qwssjv NaIC,x5SU2bybYMl Fh8IwV oGhEwVjdQnqZDY,Dv3UI,pKQjKvY14FNfgT77YKj7 XTR,zhB
9A49,PT8JNlNWciIevXQPywV ufB 1 H SpKnDmy8tCDit6CbPzy5676 rMIia6dLcmtN5,E JK,yb

Popular posts from this blog

Rothschild family

Boo (programming language)