Winsome
Winsome

Reputation: 89

Recursively iterate over a nested object to change a key value on all occurrences (JS)

I've got this confusing data object that inside it contains objects within objects and objects within arrays because it gets returned as one big blob of data. I'm trying to figure out how to replace the value for all occurrences of link in the attributes object with something else, but I'm having a difficult time getting my head around how to create a flexible solution that can step into an array or object to check if link exists.

Here's an example of the data:

const data = {
  components: [
    {
      name: 'header',
      attributes: {
        messageBars: [],
        link: '/link/'
        navigation: {
          brand: {
            link: '/',
            alt: 'blah',
          },
        },
      },
    },
    {
      name: 'body',
      attributes: {
        header: {
          text: 'blah',
        },
        buttons: [
          {
            children: 'OK!',
            link: '/link/',
          },
        ],
      },
    },

I got as far as getting into the attributes layer and got stuck how to create a recursive function called readData.

const replaceLink = (newLink: string, data: object) => {
  data.components.forEach(component => {
     if(component.attributes) readData(component.attributes, newLink);
  });

  return data;
};

Upvotes: 1

Views: 483

Answers (5)

Scott Sauyet
Scott Sauyet

Reputation: 50787

I find it helps to separate out the recursive traversal and object transformation from the specifics of what you're trying to do. Here I write a helper function replaceVal, which traverses an object and calls your callback with every nested key-value pair, allowing you to make whatever transformation you would like on the value.

Then we can write a replaceLink function as we want. It's not clear to me what you actually want to do in replacing your link. I write a version with a callback that checks if the key is "link" and the value is a String and creates an updated value by appending the uppercase version of the existing value to a fixed prefix. But you can do whatever you need here.

// helper function
const replaceVal = (f) => (o) =>
  Array .isArray (o) 
    ? o .map (replaceVal (f))
  : Object (o) === o
    ? Object .fromEntries (Object .entries (o) .map (([k, v]) => [k, replaceVal (f) (f(k, v))]))
    : o

// main function
const replaceLink = replaceVal (
  (k, v) => k == "link" && String(v) === v ? `new/path/to${v.toUpperCase()}` : v
)

// test data
const data = {components: [{name: "header", attributes: {messageBars: [], link: "/link/", navigation: {brand: {link: "/", alt: "blah"}}}}, {name: "body", attributes: {header: {text: "blah"}, buttons: [{children: "OK!", link: "/link/"}]}}]}

// demo
console .log (replaceLink (data))
.as-console-wrapper {max-height: 100% !important; top: 0}

Upvotes: 1

Varun Suresh Kumar
Varun Suresh Kumar

Reputation: 879

If you wish to just find what links are present. This prints all the links from data if it exists.

const data = {
  components: [{
      name: 'header',
      attributes: {
        messageBars: [],
        link: '/link/',
        navigation: {
          brand: {
            link: '/',
            alt: 'blah'
          }
        }
      }
    },
    {
      name: 'body',
      attributes: {
        header: {
          text: 'blah'
        },
        buttons: [{
          children: 'OK!',
          link: '/link/'
        }]
      }
    }
  ]
}
const findLinks = (elem) => {

  if (Array.isArray(elem)) {
    elem.forEach(e => findLinks(e))
  } else if (elem instanceof Object) {
    if (elem.hasOwnProperty('link')) {
      console.log('link found', elem.link, elem)
    }
    for (const key in elem) {
      findLinks(elem[key])
    }
  }
}
findLinks(data)

Upvotes: 2

Carsten Massmann
Carsten Massmann

Reputation: 28196

I know, this is a bit cheeky, but you can also do the whole thing by turning the object structure into a JSON, replace the links and then convert it back again:

const data = {
  components: [
    {
      name: 'header',
      attributes: {
        messageBars: [],
        link: '/link/',
        navigation: {
          brand: {
            link: '/',
            alt: 'blah',
          },
        },
      },
    },
    {
      name: 'body',
      attributes: {
        header: {
          text: 'blah',
        },
        buttons: [
          {
            children: 'OK!',
            link: '/link/',
          },
        ],
      },
    },
  ]
};



const changeLinks=newlink=>JSON.parse(JSON.stringify(data).replace(/"link":"[^"]*"/g,'"link":"'+newlink+'"'))

console.log(changeLinks("abc"))

Upvotes: 2

fireking-design
fireking-design

Reputation: 361

You can do it this way as well

const data = {
  components: [{
      name: 'header',
      attributes: {
        messageBars: [],
        link: '/link/',
        navigation: {
          brand: {
            link: '/',
            alt: 'blah'
          }
        }
      }
    },
    {
      name: 'body',
      attributes: {
        header: {
          text: 'blah'
        },
        buttons: [{
          children: 'OK!',
          link: '/link/'
        }]
      }
    }
  ]
}

function replaceLink(newLink, object) {
  if (Array.isArray(object)) {
    object.forEach(item => {
      if (Object.prototype.toString.call(item) === '[object Object]' || Array.isArray(item)) {
        replaceLink(newLink, item);
      }
    });
  } else {
    for (item in object) {
      if (item == "link") {
        object[item] = newLink;
      }
      if (Object.prototype.toString.call(object[item]) === '[object Object]' || Array.isArray(object[item])) {
        replaceLink(newLink, object[item]);
      }
    }
  }
}

replaceLink("newLink", data);

console.log(data);

Upvotes: 1

Robin Zigmond
Robin Zigmond

Reputation: 18249

Here is one way to do it recursively. It's a little messier than I'd hoped for, but it does the job and shouldn't be too hard to understand I hope. If the value is an array, it calls itself for each item of the array. If it's a non-array object, it replaces any value with the key "link", and otherwise calls itself recursively on all the values. For primitive values (non-objects), it leaves them unchanged.

Note that this might not behave as expected if there is ever a "link" key which holds an object or array (as that whole object/array will be replaced, rather than anything recursive going on) - I assume you know that isn't going to happen, but if it is it shouldn't be too hard to adapt this.

const data = {
  components: [
    {
      name: 'header',
      attributes: {
        messageBars: [],
        link: '/link/',
        navigation: {
          brand: {
            link: '/',
            alt: 'blah',
          },
        },
      },
    },
    {
      name: 'body',
      attributes: {
        header: {
          text: 'blah',
        },
        buttons: [
          {
            children: 'OK!',
            link: '/link/',
          },
        ],
      },
    },
  ]
};

const replaceLink = (newLink, value) => {
  if (Array.isArray(value)) {
    return value.map(item => replaceLink(newLink, item));
  }
  else if (value instanceof Object) {
    const replacement = { ...value };
    for (const key in replacement) {
      if (key === 'link') {
        replacement[key] = newLink;
      }
      else {
        replacement[key] = replaceLink(newLink, replacement[key]);
      }
    }
    return replacement;
  }
  return value;
};

const newData = { components: replaceLink('replacement link', data.components) };

console.log(newData);

Upvotes: 1

Related Questions