0

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

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.

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?


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.

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.

invot
  • 533
  • 7
  • 30
  • 4
    lodash has a function called set, that might be what your after -> https://lodash.com/docs/4.17.10#set – Keith Jun 22 '18 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 '18 at 14:45
  • you could change the object, and then you're going to save the object in localStorage – JackNavaRow Jun 22 '18 at 14:47
  • @JackNavaRow: could you provide a bit more detail? I thought that's what I was doing :/ – invot Jun 22 '18 at 14:49
  • 2
    `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 '18 at 14:51
  • 1
    What you seem to be asking currently is to traverse your object from a dot notation string. Then it is a duplicate of https://stackoverflow.com/questions/6393943/convert-javascript-string-in-dot-notation-into-an-object-reference 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 '18 at 14:23
  • @Kaiido: I keep being told that, but these answers are only explain how to **GET** a value, not **SET** a value. I'm looking to set the value. – invot Jun 25 '18 at 14:28
  • Just don't... You are deeply in a X-Y problem. – Kaiido Jun 25 '18 at 14:33
  • To set it, parse the JSON string from localStorage into an Object, traverse the object, set the new values, re-stringify back to JSON and save it in localStorage. – jjbskir Jun 25 '18 at 14:33
  • @jjbskir: that's what I'm doing. However, I'm trying to traverse it at a variable depth. I need to write a function that can handle finding a property anywhere in the object, no matter how nested it is. – invot Jun 25 '18 at 14:35
  • @Kaiido: There are not many options I have when it comes to design patterns. I'm looking for ways to store and update objects kept in `localStorage` as that's the only method that fulfills a very strict set of requirements for this project. I already have a working method to read the properties stored. I'm looking for a BETTER way to update those properties. – invot Jun 25 '18 at 14:42
  • But you do have a pattern problem. You can't arbitrarily set a nested object only by defining its path from nowhere. What if user A creates `a.b.c` and then user B creates `a.b.d` should `a.b` be the same object in both cases? What if A creates an Array and B an object `{a:"foo", 0:"bar"}` at the same path? Better to be stricter in your setting function, you will avoid a lot of coming overheads. – Kaiido Jun 25 '18 at 14:54
  • Well, in the real code, we are checking against a schema to make sure the value being set or retrieved is valid, but I pruned that bit out to help focus on the issue I'm trying to solve. – invot Jun 25 '18 at 15:08
  • When I first looked at this, I thought this looks like a case for recursion.. But when I looked at it in a bit more depth, I suspect they way you are using dot notation is your enemy, Can't you just use user as a key and store a JSON object? – JGFMK Jun 26 '18 at 21:31

6 Answers6

1

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:

  1. dist variable is created
  2. hardcoded switch is replaced with foreach
Nodarii
  • 934
  • 7
  • 21
0

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);
Wouter Coebergh
  • 804
  • 1
  • 6
  • 20
  • I don't understand what your code does. I can't get it to update a value. http://jsbin.com/qodogibibe/edit?js,console – invot Jun 29 '18 at 14:49
0

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.

chriopp
  • 947
  • 7
  • 12
0

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.

IAMSAMMM
  • 385
  • 3
  • 5
0

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.

Takit Isy
  • 9,688
  • 3
  • 23
  • 47
0

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);
Vic
  • 43
  • 4