Donkey Shame
Donkey Shame

Reputation: 764

How do I recurse a nested object value such that the parent object contains a corresponding named collection?

Okay, first of all: if I could have phrased my question in a less awkwardly obtuse manner, I would have done so. 😜

I'm struggling with how to describe what I'm trying to do.

Goal

I'm attempting to iterate over an object (specifically, the colors object in the default TailwindCss config) in order to generate a list of tokens I can pull into a nunjucks template. This is proving more challenging than I'd expected.

I'm probably missing something basic, but I don't know what I don't know, ya know?

I've got the following function that just loops over the colors object:

const tailwindSrc = require("../node_modules/tailwindcss/defaultConfig.js");
const colors = tailwindSrc.theme.colors;

const iterateObject = function (object) {
    let tokenList = [];
    Object.keys(object).forEach((key) => {
        Object.keys(object[key]).forEach((subkey) => {
            tokenList.push({
                token: {
                collectionKey: key,
                key: subkey,
                value: object[key][subkey],
                },
            });
        });
    });
    return tokenList;
  };

  iterateObject(colors);

This is a representative portion of my current result 👇

{ token: { collectionKey: 'gray', key: '50',  value: '#f9fafb' } },
{ token: { collectionKey: 'gray', key: '100', value: '#f3f4f6' } },
{ token: { collectionKey: 'gray', key: '200', value: '#e5e7eb' } },
{ token: { collectionKey: 'gray', key: '300', value: '#d1d5db' } },
{ token: { collectionKey: 'gray', key: '400', value: '#9ca3af' } },
{ token: { collectionKey: 'gray', key: '500', value: '#6b7280' } },
{ token: { collectionKey: 'gray', key: '600', value: '#4b5563' } },
{ token: { collectionKey: 'gray', key: '700', value: '#374151' } },
{ token: { collectionKey: 'gray', key: '800', value: '#1f2937' } },
{ token: { collectionKey: 'gray', key: '900', value: '#111827' } }

But I want the gray "collection" to have a value that is itself an object containing the other two name-value pairs.

Something sort of like the following, I guess. I'm not hung up on the structure below at all; just trying to illustrate more or less how I need things to be grouped.

{
  token: { 
      collectionKey: 'gray',
      collectionEntries: {
          item: { key: '100', value: '#f3f4f6' }
          item: { key: '200', value: '#e5e7eb' }
          item: { key: '300', value: '#d1d5db' }
          item: { key: '400', value: '#9ca3af' }
          item: { key: '500', value: '#6b7280' }
          item: { key: '600', value: '#4b5563' }
          item: { key: '700', value: '#374151' }
          item: { key: '800', value: '#1f2937' }
          item: { key: '900', value: '#111827' }
      }
  }
}

I've made probably 8-10 tries to accomplish what I'm after over the past two days. No dice so far.

It seems to me that I'm not recursing deeply enough to accomplish what I want, but I haven't been able to make things work out in a consistent, repeatable manner as I've tried to go deeper. I asked lodash for help and it laughed at me. I've even tried to create two objects that contain bits of what I need and then merge them together (a la this). No dice.

So, I now turn to you, dear reader.

Sure, a complete solution would be great. But I'll also gladly take links, suggestions, warnings, observations, etc. Any help welcome.

Thanks!


UPDATE

Thanks for your help, Amir!

What you've provided above looks promising. It's not quite there yet, but it's a step in the right direction, I think.

Here's what I'm getting with your code:

[
  {
    collectionKey: 'pink',
    collectionEntries: [
      [Object], [Object],
      [Object], [Object],
      [Object], [Object],
      [Object], [Object],
      [Object], [Object]
    ]
  },
...
]

I should note that every collectionKey in the series is 'pink'. 😁

Looks an array of arrays of objects. Hmm.

I should also note that I changed one line of your code from

let token: {};

to

let token = {};

Any idea what's goin awry above?

Thanks, again!


UPDATE #2

Amir, you asked to see the structure of the data I'm working with. I've simplified things (I hope) by importing the colors file directly, as opposed to the entire tailwind config.

Here it is:

module.exports = {
  black: '#000',
  white: '#fff',
  rose: {
    50: '#fff1f2',
    100: '#ffe4e6',
    200: '#fecdd3',
    300: '#fda4af',
    400: '#fb7185',
    500: '#f43f5e',
    600: '#e11d48',
    700: '#be123c',
    800: '#9f1239',
    900: '#881337',
  },
  pink: {
    50: '#fdf2f8',
    100: '#fce7f3',
    200: '#fbcfe8',
    300: '#f9a8d4',
    400: '#f472b6',
    500: '#ec4899',
    600: '#db2777',
    700: '#be185d',
    800: '#9d174d',
    900: '#831843',
  },

  ...

};

Note that some entries are not nested (black and white, for example), but the remainder of them are.

It would great to be able to accomodate differences of that sort. Have tried wrapping two different .push methods in if statements, but it seems like when I fix one use case, I break the other.

Thanks again for your help!

Upvotes: 2

Views: 199

Answers (4)

vincent
vincent

Reputation: 2181

Here is an iterative solution that uses object-scan

First we collect all the necessary entries and then we remap to an array

Note that since no "key" exists for simple colors I just left it as null

// const objectScan = require('object-scan');

const myColors = {transparent: "transparent", current: "currentColor", black: "#000", white: "#fff", gray: {50: "#f9fafb", 100: "#f3f4f6", 200: "#e5e7eb", 300: "#d1d5db", 400: "#9ca3af", 500: "#6b7280", 600: "#4b5563", 700: "#374151", 800: "#1f2937", 900: "#111827"}, red: {50: "#fef2f2", 100: "#fee2e2", 200: "#fecaca", 300: "#fca5a5", 400: "#f87171", 500: "#ef4444", 600: "#dc2626", 700: "#b91c1c", 800: "#991b1b", 900: "#7f1d1d"}, yellow: {50: "#fffbeb", 100: "#fef3c7", 200: "#fde68a", 300: "#fcd34d", 400: "#fbbf24", 500: "#f59e0b", 600: "#d97706", 700: "#b45309", 800: "#92400e", 900: "#78350f"}, green: {50: "#ecfdf5", 100: "#d1fae5", 200: "#a7f3d0", 300: "#6ee7b7", 400: "#34d399", 500: "#10b981", 600: "#059669", 700: "#047857", 800: "#065f46", 900: "#064e3b"}, blue: {50: "#eff6ff", 100: "#dbeafe", 200: "#bfdbfe", 300: "#93c5fd", 400: "#60a5fa", 500: "#3b82f6", 600: "#2563eb", 700: "#1d4ed8", 800: "#1e40af", 900: "#1e3a8a"}, indigo: {50: "#eef2ff", 100: "#e0e7ff", 200: "#c7d2fe", 300: "#a5b4fc", 400: "#818cf8", 500: "#6366f1", 600: "#4f46e5", 700: "#4338ca", 800: "#3730a3", 900: "#312e81"}, purple: {50: "#f5f3ff", 100: "#ede9fe", 200: "#ddd6fe", 300: "#c4b5fd", 400: "#a78bfa", 500: "#8b5cf6", 600: "#7c3aed", 700: "#6d28d9", 800: "#5b21b6", 900: "#4c1d95"}, pink: {50: "#fdf2f8", 100: "#fce7f3", 200: "#fbcfe8", 300: "#f9a8d4", 400: "#f472b6", 500: "#ec4899", 600: "#db2777", 700: "#be185d", 800: "#9d174d", 900: "#831843"}};

const remap = (colors) => {
  const result = objectScan(['*', '*.*'], {
    filterFn: ({ key, value, isLeaf, context }) => {
      if (!(key[0] in context)) {
        context[key[0]] = [];
      }
      if (isLeaf) {
        context[key[0]].push({
          key: key[1] || null,
          value
        });
      }
    }
  })(colors, {});
  return Object.entries(result).map(([k, v]) => ({ token: { [k]: v } }));
};

console.log(remap(myColors));
// => [ { token: { pink: [ { key: '900', value: '#831843' }, { key: '800', value: '#9d174d' }, { key: '700', value: '#be185d' }, { key: '600', value: '#db2777' }, { key: '500', value: '#ec4899' }, { key: '400', value: '#f472b6' }, { key: '300', value: '#f9a8d4' }, { key: '200', value: '#fbcfe8' }, { key: '100', value: '#fce7f3' }, { key: '50', value: '#fdf2f8' } ] } }, { token: { purple: [ { key: '900', value: '#4c1d95' }, { key: '800', value: '#5b21b6' }, { key: '700', value: '#6d28d9' }, { key: '600', value: '#7c3aed' }, { key: '500', value: '#8b5cf6' }, { key: '400', value: '#a78bfa' }, { key: '300', value: '#c4b5fd' }, { key: '200', value: '#ddd6fe' }, { key: '100', value: '#ede9fe' }, { key: '50', value: '#f5f3ff' } ] } }, { token: { indigo: [ { key: '900', value: '#312e81' }, { key: '800', value: '#3730a3' }, { key: '700', value: '#4338ca' }, { key: '600', value: '#4f46e5' }, { key: '500', value: '#6366f1' }, { key: '400', value: '#818cf8' }, { key: '300', value: '#a5b4fc' }, { key: '200', value: '#c7d2fe' }, { key: '100', value: '#e0e7ff' }, { key: '50', value: '#eef2ff' } ] } }, { token: { blue: [ { key: '900', value: '#1e3a8a' }, { key: '800', value: '#1e40af' }, { key: '700', value: '#1d4ed8' }, { key: '600', value: '#2563eb' }, { key: '500', value: '#3b82f6' }, { key: '400', value: '#60a5fa' }, { key: '300', value: '#93c5fd' }, { key: '200', value: '#bfdbfe' }, { key: '100', value: '#dbeafe' }, { key: '50', value: '#eff6ff' } ] } }, { token: { green: [ { key: '900', value: '#064e3b' }, { key: '800', value: '#065f46' }, { key: '700', value: '#047857' }, { key: '600', value: '#059669' }, { key: '500', value: '#10b981' }, { key: '400', value: '#34d399' }, { key: '300', value: '#6ee7b7' }, { key: '200', value: '#a7f3d0' }, { key: '100', value: '#d1fae5' }, { key: '50', value: '#ecfdf5' } ] } }, { token: { yellow: [ { key: '900', value: '#78350f' }, { key: '800', value: '#92400e' }, { key: '700', value: '#b45309' }, { key: '600', value: '#d97706' }, { key: '500', value: '#f59e0b' }, { key: '400', value: '#fbbf24' }, { key: '300', value: '#fcd34d' }, { key: '200', value: '#fde68a' }, { key: '100', value: '#fef3c7' }, { key: '50', value: '#fffbeb' } ] } }, { token: { red: [ { key: '900', value: '#7f1d1d' }, { key: '800', value: '#991b1b' }, { key: '700', value: '#b91c1c' }, { key: '600', value: '#dc2626' }, { key: '500', value: '#ef4444' }, { key: '400', value: '#f87171' }, { key: '300', value: '#fca5a5' }, { key: '200', value: '#fecaca' }, { key: '100', value: '#fee2e2' }, { key: '50', value: '#fef2f2' } ] } }, { token: { gray: [ { key: '900', value: '#111827' }, { key: '800', value: '#1f2937' }, { key: '700', value: '#374151' }, { key: '600', value: '#4b5563' }, { key: '500', value: '#6b7280' }, { key: '400', value: '#9ca3af' }, { key: '300', value: '#d1d5db' }, { key: '200', value: '#e5e7eb' }, { key: '100', value: '#f3f4f6' }, { key: '50', value: '#f9fafb' } ] } }, { token: { white: [ { key: null, value: '#fff' } ] } }, { token: { black: [ { key: null, value: '#000' } ] } }, { token: { current: [ { key: null, value: 'currentColor' } ] } }, { token: { transparent: [ { key: null, value: 'transparent' } ] } } ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>

Disclaimer: I'm the author of object-scan

Upvotes: 0

Amir BenAmara
Amir BenAmara

Reputation: 686

you can do things like that :

const tailwindSrc = require("../node_modules/tailwindcss/defaultConfig.js");
const colors = tailwindSrc.theme.colors;

const iterateObject = function (object) {
  let tokenList = [];
  let token = {}
  Object.keys(object).forEach((key) => {
    token['collectionKey']= key,
    token['collectionEntries'] = []
    Object.keys(object[key]).forEach((subkey) => {
      token.collectionEntries.push({
        key: subkey,
        value: object[key][subkey],
      });
    });
    tokenList.push({token : token});
  });
  return tokenList;
};

iterateObject(colors);

but result here will be :

{
  token: { 
      collectionKey: 'gray',
      collectionEntries: [
          { key: '100', value: '#f3f4f6' }
          { key: '200', value: '#e5e7eb' }
          { key: '300', value: '#d1d5db' }
          { key: '400', value: '#9ca3af' }
          { key: '500', value: '#6b7280' }
          { key: '600', value: '#4b5563' }
          { key: '700', value: '#374151' }
          { key: '800', value: '#1f2937' }
          { key: '900', value: '#111827' }
      ]
  }
}

so you can avoid duplication of key : 'item'

Upvotes: 1

Scott Sauyet
Scott Sauyet

Reputation: 50797

Now that we have your input format, we can convert it fairly simply.

Here is one technique, assuming that the entries like transparent and black should return output as much like the others as possible, and further assuming that collectionEntries should be an array. (Your format doesn't work, repeating the key item over and over in your object.)

const collectColors = (colors) => Object .entries (colors)
  .map (([k, v]) => (
    Object (v) === v
      ? {token: { 
          collectionKey: k,
          collectionEntries: Object .entries (v) .map (([key, value]) => ({item: {key, value}}))
        }} 
      : {token: {collectionKey: k, collectionEntries: []}}
  ))

const colors = {transparent: "transparent", current: "currentColor", black: "#000", white: "#fff", gray: {50: "#f9fafb", 100: "#f3f4f6", 200: "#e5e7eb", 300: "#d1d5db", 400: "#9ca3af", 500: "#6b7280", 600: "#4b5563", 700: "#374151", 800: "#1f2937", 900: "#111827"}, red: {50: "#fef2f2", 100: "#fee2e2", 200: "#fecaca", 300: "#fca5a5", 400: "#f87171", 500: "#ef4444", 600: "#dc2626", 700: "#b91c1c", 800: "#991b1b", 900: "#7f1d1d"}, yellow: {50: "#fffbeb", 100: "#fef3c7", 200: "#fde68a", 300: "#fcd34d", 400: "#fbbf24", 500: "#f59e0b", 600: "#d97706", 700: "#b45309", 800: "#92400e", 900: "#78350f"}, green: {50: "#ecfdf5", 100: "#d1fae5", 200: "#a7f3d0", 300: "#6ee7b7", 400: "#34d399", 500: "#10b981", 600: "#059669", 700: "#047857", 800: "#065f46", 900: "#064e3b"}, blue: {50: "#eff6ff", 100: "#dbeafe", 200: "#bfdbfe", 300: "#93c5fd", 400: "#60a5fa", 500: "#3b82f6", 600: "#2563eb", 700: "#1d4ed8", 800: "#1e40af", 900: "#1e3a8a"}, indigo: {50: "#eef2ff", 100: "#e0e7ff", 200: "#c7d2fe", 300: "#a5b4fc", 400: "#818cf8", 500: "#6366f1", 600: "#4f46e5", 700: "#4338ca", 800: "#3730a3", 900: "#312e81"}, purple: {50: "#f5f3ff", 100: "#ede9fe", 200: "#ddd6fe", 300: "#c4b5fd", 400: "#a78bfa", 500: "#8b5cf6", 600: "#7c3aed", 700: "#6d28d9", 800: "#5b21b6", 900: "#4c1d95"}, pink: {50: "#fdf2f8", 100: "#fce7f3", 200: "#fbcfe8", 300: "#f9a8d4", 400: "#f472b6", 500: "#ec4899", 600: "#db2777", 700: "#be185d", 800: "#9d174d", 900: "#831843"}}

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

It's easy enough to make variants. If you just want to skip the ones like transparent, then you use this variant:

const collectColors = (colors) => Object .entries (colors)
  .filter (([_, v]) => Object (v) === v)
  .map (([k, v]) => ({
    token: { 
      collectionKey: k,
      collectionEntries: Object .entries (v) .map (([key, value]) => ({item: {key, value}}))
    }
  }))

Or if you wanted collectionEntries to be an array containing key-value objects (what is the use of that extra items wrapper anyway?), you could replace the relevant line with

            collectionEntries: Object .entries (v) .map (([key, value]) => ({key, value}))

And so forth.

Upvotes: 1

Blujedis
Blujedis

Reputation: 366

Unless I'm missing something it shouldn't be much more than something like this.

Please note in your example or desired results that would likely need to be an array as you are duplicating the object key "item". The below will produce an object as you wish the but the key will be the numeric value. In the case of those values that aren't objects I just chose to use the numeric value 0 but you could set that to whatever you wish.

const tokens = Object.keys(obj).reduce((result, key) => {

  let val = obj[key];

  // Used key as "0" here but you should define
  // what you want that to be when they aren't 
  // objects.
  if (typeof val === 'string')
    val = { 0: val };
  
  result[key] = {
    collectionKey: key,
    collectionEntries: Object.keys(val).reduce((items, k) => {
      // parseInt here to maintain as numeric value.
      items[k] = { key: parseInt(k), value: val[k] };
      return items;
    }, {})
  };
  return result;
}, {});

Upvotes: 0

Related Questions