2

Given an object shaped like the following, (which can have unknown number of nested properties)

const theme = {
    fonts: {
        primary: 'Arial',
        secondary: 'Helvetica'
    },
    colors: {
        primary: 'green',
        secondary: 'red',
    },
    margin: {
        small: '0.5rem',
        medium: '1rem',
        large: '1.5rem'
    }
}

Im trying to achieve the following:

  1. Loop through recursively until i hit a value that isn't an object
  2. When i hit this value i would like to have access to all the keys that lead up to it, along with the end value.

Something like the following:

['fonts', 'primary'], 'Arial'

['fonts', 'secondary'] 'Helvetica'

['colors', 'primary'] 'green'

etc.

I have tried various different attempts, but the bit that is stumping me is how do i keep track of the keys, and reset them when the original loop is called again?

Samuel
  • 2,485
  • 5
  • 30
  • 40

4 Answers4

1

You could take a recursive approach and check for the object and map the nested subresults to the final result.

const
    getPathes = object => Object
        .entries(object)
        .reduce((r, [k, v]) => {
            if (v && typeof v === 'object') {
                r.push(...getPathes(v).map(([p, v]) => [[k, ...p], v]));
            } else {
                r.push([[k], v]);
            }
            return r;
        }, []),
    theme = { fonts: { primary: 'Arial', secondary: 'Helvetica' }, colors: { primary: 'green', secondary: 'red' }, margin: { small: '0.5rem', medium: '1rem', large: '1.5rem' } };
    
console.log(getPathes(theme));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
1

Thanks all for your responses really helped me get there, i ended up with this:

const setCSSVars = (obj: { [key: string]: any }, stack: string[] = []) => {
  Object.entries(obj).forEach(([key, value]) => {
    if (typeof value === 'object' && value !== null) {
      setCSSVars(value, [...stack, key])
    } else {
      document.documentElement.style.setProperty(
        `--theme-${stack.join('-')}-${key}`,
        value
      )
    }
  })
}

Samuel
  • 2,485
  • 5
  • 30
  • 40
0

Here's an alternative approach.

You could create a function that uses for of loop to iterate over the keys of the object passed as an argument. Then push the current key in to an array that temporarily holds the keys. After that, check if current key has a value that is not an object. If this condition is true, push an object into an array, created outside of the function, that has value of the current key as a key and its value should be an array that contains all the keys leading up to the current key's value.

If the current key's value is not an object, call the function recursively and repeat the process.

P.S: I have used the following output format.

[ { "Arial": [ "fonts", "primary" ] }, { "Helvetica": [ "fonts", "primary" ] } ]

const theme = {
  fonts: { primary: 'Arial', secondary: 'Helvetica' },
  colors: { primary: 'green', secondary: 'red' },
  margin: { small: '0.5rem', medium: '1rem', large: '1.5rem' }
};

const keysArr = [];

function createKeyPath(obj, tempKeysArr = []) {
  for (const k of Object.keys(obj)) {
    // push current key in temporary array
    tempKeysArr.push(k);

    if (typeof obj[k] != 'object') {
      // push a new object in keysArr
      keysArr.push({ [obj[k]]: [...tempKeysArr] });
      // remove last key from temporary key array
      tempKeysArr.pop();
    } else {
      createKeyPath(obj[k], tempKeysArr);
      // reset tempKeysArr
      tempKeysArr = [];
    }
  }
}

createKeyPath(theme);
console.log(keysArr);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Yousaf
  • 27,861
  • 6
  • 44
  • 69
0

Another simple recursive approach would look like this:

const pathEntries = (obj) =>
  Object (obj) === obj
    ? Object .entries (obj) .flatMap (
        ([k, x]) => pathEntries (x) .map (([p, v]) => [[k, ... p], v])
      ) 
    : [[[], obj]]

const theme = {fonts: {primary: 'Arial', secondary: 'Helvetica'}, colors: {primary: 'green', secondary: 'red'}, margin: {small: '0.5rem', medium: '1rem', large: '1.5rem'}}

console.log(pathEntries(theme))
.as-console-wrapper {max-height: 100% !important; top: 0}

If the value supplied is not an object or array, then we just return the object wrapped up like the final format, with an empty path: [[], obj]. If it is an object or array, we flat map over the entries of the object, recurring on their values, and prepending their keys to each of the resulting paths captured in the recursive call.


But I would prefer to build this out of more reusable pieces.

const getPaths = (obj) =>
  Object (obj) === obj
    ? Object .entries (obj) .flatMap (([k, v]) => getPaths (v) .map (p => [k, ... p]))
    : [[]]

const path = (ps) => (obj) => 
  ps .reduce ((o, p) => (o || {}) [p], obj)

const pathEntries = (obj) => 
  getPaths (obj) .map (p => [p, path (p) (obj)])

const theme = {fonts: {primary: 'Arial', secondary: 'Helvetica'}, colors: {primary: 'green', secondary: 'red'}, margin: {small: '0.5rem', medium: '1rem', large: '1.5rem'}}

console .log (pathEntries (theme))
.as-console-wrapper {max-height: 100% !important; top: 0}
  • getPaths finds the paths you're looking for (without the values), yielding something like [["fonts", "primary"], ["fonts", "secondary"], ..., ["margin", "large"]].

  • path takes a path in that format and returns a function which takes an object, returning the value found in that object along the given path, or undefined if any of the intermediate nodes are missing.

  • pathEntries is the main function, which first uses getPaths to find all the leaf paths in the object and then maps each path to your output format by using path.

In either case, pathEntries is specific to your somewhat unusual output format. But for the second approach, the two helper functions are quite useful in many circumstances.

But if choosing the second approach, you should be aware that it will not perform as well as the first. It's scanning the object multiple times, once to find the paths, and performing a partial traversal for each of those paths. I would still choose it over the other for the reusable parts unless profiling told me that it was a bottleneck in my codebase. But you should bear it in mind.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103