Creating a recursive function in JavaScript to build a menu list object

Having an assortment of

['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

The objective is to create a map object that mirrors this structure:

{
    'social': {
        swipes: {
            women: null,
            men: null
        }
    },
    'upgrade': {
        premium: null
    }
}

const menu = ['/social/swipes/women', '/social/likes/men', '/upgrade/premium'];
const map = {};

const addLabelToMap = (root, label) => {
  if(!map[root]) map[root] = {};
  if(!map[root][label]) map[root][label] = {};
}

const buildMenuMap = menu => {
  menu
    // make a copy of the menu
    .slice()
    // convert each string into an array by splitting at /
    .map(item => item.split('/').splice(1))
    // loop over each array and its elements
    .forEach((element) => {
      let root = map[element[0]] || "";

      for (let i = 1; i < element.length; i++) {
        const label = element[i];
        addLabelToMap(root, label)
        root = root[label];
      }
    });
}

buildMenuMap(menu);

console.log(map);

However, there seems to be confusion on how to update the value of root.

What should I set root to in order to correctly call addLabelToMap with

'[social]',

'swipes' => '[social][swipes]'
, 'women' => '[social][swipes]', 'men'?

I attempted using root = root[element] but it resulted in an error.

If you have alternative solutions, feel free to suggest them. It's important for me to grasp why this approach is not functioning as intended.

Answer №1

The task at hand revolves around creating an object and maintaining its state while iterating through the "input" array, splitting strings based on the "/" character.

One way to achieve this is by using the built-in method `Array.reduce`. By initializing an empty object and filling it during the iteration of the "input" array, we can ensure that for the last word in each string, the object property is assigned a value of `null`.

let input = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

let output = input.reduce((o, d) => {
  let keys = d.split('/').filter(d => d)
  
  keys.reduce((k, v, i) => {
    k[v] = (i != keys.length - 1)
             ? k[v] || {} 
             : null
    
    return k[v]
  }, o)
  
  return o
}, {})

console.log(output)

Answer №2

Utilize the reduce method instead of map. In this scenario, the root serves as the accumulator:

const buildMenuMap = menu =>
  menu.reduce((root, item) => {
    let parts = item.slice(1).split("/");
    let lastPart = parts.pop();
    let leaf = parts.reduce((acc, part) => acc[part] || (acc[part] = {}), root);
    leaf[lastPart] = null;
    return root;
  }, Object.create(null));

Explanation:

When iterating through each item in the menu array, we extract the parts by removing the leading '/' (using slice(1)) and then splitting by '/'.

The lastPart from this array is removed (the final part is handled separately).

For every remaining part in the parts array, we traverse the root object. At each level of traversal, we either return the existing object at that level acc[part] if it already exists, or we create and return a new one if it doesn't (acc[part] = {}).

Once reaching the final level leaf, the value is set to null using the lastPart.

It's important to note that Object.create(null) is passed to reduce to create a prototypeless object. This makes it safer to access keys in root[someKey] without needing to check if someKey is an owned property.

Example:

const buildMenuMap = menu =>
  menu.reduce((root, item) => {
    let parts = item.slice(1).split("/");
    let lastPart = parts.pop();
    let leaf = parts.reduce((acc, part) => acc[part] || (acc[part] = {}), root);
    leaf[lastPart] = null;
    return root;
  }, Object.create(null));

let arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

let result = buildMenuMap(arr);

console.log(result);

Answer №3

Here's a simple way to achieve this:

 root = root[label];

If you adjust your helper function like so:

 const addLabelToMap = (root, label) => {
    if(!root[label]) root[label] =  {};
 }

This is how I would approach it:

 const buildMenuMap = menus => {
   const root = {};

   for(const menu of menus) {
     const keys = menu.split("/").slice(1);
     const prop = keys.pop();
     const obj = keys.reduce((curr, key) => curr[key] || (curr[key] = {}), root);
     obj[prop] = null;
  }

  return root;
}

Answer №4

I recently analyzed your code to identify the issues and I recommend you do the same. There are two glaring mistakes:

Firstly, in the initial iteration, the value of map is simply an empty object {}, while the values of root and label are initialized as "" and swipes respectively.

.forEach((element) => {
  let root = map[element[0]] || "";
  ...
  root = root[label];
}

This results in root[label] being undefined, leading to the new root also being undefined.

Secondly, you are using map in its original form throughout.

const addLabelToMap = (root, label) => {
  if(!map[root]) map[root] = {};
  if(!map[root][label]) map[root][label] = {};
}

Instead, it should be passed as a parameter for recursive purposes.

const addLabelToMap = (root, label) => {
  if(!root[label]) root[label] = {};
}

To debug your code, create a basic HTML file with the JavaScript enclosed in script tags and then host it locally using python -m http.server. This will allow you to set up breakpoints and analyze your code step by step.

Answer №5

Here's a comprehensive approach to tackle this:

const menuItems = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

const mergeObjects = (target, source) => {
  // Traverse through `source` properties and if an `Object`, combine properties of `target` and `source`
  for (let key of Object.keys(source)) {
    if (source[key] instanceof Object && key in target) Object.assign(source[key], mergeObjects(target[key], source[key]))
  }

  // Combine `target` and modified `source`
  Object.assign(target || {}, source)
  return target
};

const createMenuMap = menuItems => {
  return menuItems
    .map(item => item.split('/').splice(1))

    // The `root` value is the object where all directories will be merged into
    .reduce((root, directory) => {

      // Iterates backwards through each directory array, stacking the previous accumulated object into the current one
      const branch = directory.slice().reverse().reduce((acc, cur) => { const obj = {}; obj[cur] = acc; return obj;},null);

      // Utilizes the `mergeObjects()` method to combine the accumulated `root` object with the newly created `branch` object.
      return mergeObjects(root, branch);
    }, {});
};

createMenuMap(menuItems);

Note: The deep merge concept was sourced from @ahtcx on GitHubGist

Answer №6

To streamline your code, consider utilizing Array.reduce, Object.keys & String.substring

Create Menu Map

This function takes an array as input and reduces it into an object where each entry in the array updates the object with the corresponding hierarchy using the addLabelToMap function. Each entry is converted into an array of levels (c.substring(1).split("/")).

Add Label to Map

This function has 2 inputs:

  • obj - represents the current root object / node
  • ar - an array of child hierarchy

The function then returns the updated object.

Process Logic

  • The function extracts the first value using let key = ar.shift() as the key and adds/updates it in the object (obj[key] = obj[key] || {};).
  • If there is a child hierarchy present (if(ar.length)), the function recursively calls itself to update the object until the end (addLabelToMap(obj[key], ar)).
  • If there is no further child hierarchy, check if the object has additional entries (
    else if(!Object.keys(obj[key]).length
    ). If there are no more levels, indicating a leaf node, set the value to null (obj[key] = null). Note: if there will never be a scenario like /social/swipes/men/young alongside existing entries in the array, the else if block can be simplified to just an else block.
  • Once the object is updated, return the final updated object.

let arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

function addLabelToMap(obj, ar) {
  let key = ar.shift();
  obj[key] = obj[key] || {}; 
  if(ar.length) addLabelToMap(obj[key], ar);
  else if(!Object.keys(obj[key]).length) obj[key] = null;
  return obj;
}

function buildMenuMap(ar) {
  return ar.reduce((a,c) => addLabelToMap(a,c.substring(1).split("/")), {});
}

console.log(buildMenuMap(arr));

Answer №7

If you want to tackle this problem in a more succinct manner, consider using a recursive function like the one shown below:

let obj={}, input = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];

const makeObj = (arr, obj={}) => {
  let prop = arr.shift()
  prop in obj ? null : Object.assign(obj, {[prop]: {}})
  arr.length ? makeObj(arr, obj[prop]) : obj[prop] = null
  return obj
}

input.forEach(x => makeObj(x.split('/').filter(Boolean), obj))

console.log(obj)

The concept involves sending each path to the makeObj function, which recursively adds the paths to an object until it reaches the end of the path array. This approach provides an alternative to using the commonly used Array.reduce method.

Answer №8

A quote taken from the bounty description:
The existing answers lack sufficient detail.

It appears that you might be struggling to grasp the concepts presented in the current answers. To assist you, I will offer two solutions: an alternative approach and a more detailed solution involving the use of Array.reduce() function.

Alternative method using for loops

Please refer to the code comments for an explanation of the code.

var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'],
    result = {};

for(var i = 0; i < arr.length; i++)
{
    var parts = arr[i].slice(1).split('/'),
        curObj = result[parts[0]] = result[parts[0]] || {};

    for(var k = 1; k < parts.length; k++)
    {
        if(parts[k+1])
            curObj[parts[k]] = curObj[parts[k]] || {};
        else curObj[parts[k]] = null;

        curObj = curObj[parts[k]];
    }
}

console.log(JSON.stringify(result, null, 4));

If you find the above code confusing, please take a look at this simplified version:

var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'],
    result = {},
    parts = arr[2].slice(1).split('/'),
    curObj = result[parts[0]] = {};

curObj[parts[1]] = parts[1+1] ? {} : null;
console.log(JSON.stringify(result, null, 4));

Detailed solution utilizing Array.reduce()

In this enhanced solution, I have elaborated on the code provided by user Nitish Narang, including explanations in the comments and console output. If you struggle with understanding arrow functions, feel free to expand them into normal functions with descriptive variable names. Visualization aids in comprehension. Below is a slightly condensed version of their code.

var arr = ['/social/swipes/women', '/social/swipes/men', '/upgrade/premium'];
var result = arr.reduce(function(acc0, curVal, curIdx)
{
    console.log('\n' + curIdx + ': ------------\n'
                + JSON.stringify(acc0, null, 4));

    var keys = curVal.slice(1).split('/');
    keys.reduce(function(acc1, currentValue, currentIndex)
    {
        acc1[currentValue] = keys[currentIndex+1]
                                ? acc1[currentValue] || {}
                                : null;

        return acc1[currentValue]
    }, acc0); 

    return acc0
}, {}); 

console.log('\n------result------\n'
            + JSON.stringify(result, null, 4));

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Encountering an issue with Angular virtual scrolling: ViewDestroyedError arises when trying to utilize a destroyed view during detectChanges operation

My implementation involves using virtual scrolling from the cdk within a trigger-opening sidenav on a mat-radio element. Here is the code snippet: ts - ... @Component({ selector: 'app-generic-options-list', templateUrl: './generic-opt ...

Using jQuery and AJAX to send a post request in a Razor page and automatically redirect to the view returned by a MVC Action (similar to submitting

I send a json array to the MVC Action using either JQuery or Ajax, and the Action processes the request correctly. However, when the MVC Action returns a View, I am unsure of how to redirect to this View or replace the body with it. Overall, everything se ...

Fixing the Timeout Error in Node.js & Mongoose: A Step-by-Step Guide

After developing an API, I encountered the following issue: const express = require("express"); const router = express.Router(); const {getAttendanceSheet,getDailyAttendance} = require("../../services/attendanceService"); router.get(& ...

What is the process of combining two identical objects in Angular JS?

Is it possible to merge and sort two objects in Angular JS that have the same fields, notifications.fb and notifications.tw, based on their time field? ...

Submitting an array of data to an express POST endpoint

Within my mongoose model, Todo, I have a specific structure: content: [{ contentType: { type: String, default: null }, contentValue: { type: String, default: null } }] The POST route in my express applicati ...

Is there a way for me to determine the quality of a video and learn how to adjust it?

(function(){ var url = "http://dash.edgesuite.net/envivio/Envivio-dash2/manifest.mpd"; var player = dashjs.MediaPlayer().create(); player.initialize(document.querySelector("#videoPlayer"), url, })(); var bitrates = player.getBitrateInfoListFor("vid ...

Experiencing trouble with calculating the total value of an array of objects due to NaN errors

I've been working on developing this web application in VUE.js, but I'm facing an issue with a specific function where I am attempting to sum the values of each object within an array. This is resulting in a NaN (Not a Number) error. Let's t ...

Error Message: The function "menu" is not a valid function

I've encountered an issue with a function not being called properly. The error message states "TypeError: menu is not a function." I attempted to troubleshoot by moving the function before the HTML that calls it, but unfortunately, this did not resolv ...

Tips for running a dry default with Angular CLI

Query: Can dry-run be set as the default in a configuration? Purpose: Enabling dry-run by default simplifies the learning process by minimizing clean-up tasks if the command is not correct. This can encourage users to always perform a test run before exec ...

Deactivate certain days in Material UI calendar component within a React application

Currently, my DatePicker component in React js is utilizing material-ui v0.20.0. <Field name='appointmentDate' label="Select Date" component={this.renderDatePicker} /> renderDatePicker = ({ input, label, meta: { touched, error ...

Encountering error TS2307 while using gulp-typescript with requirejs and configuring multiple path aliases and packages

Currently, I am working on a substantial project that heavily relies on JavaScript. To enhance its functionality, I am considering incorporating TypeScript into the codebase. While things are running smoothly for the most part, I have encountered an issue ...

Utilizing React JS with Material-UI Autocomplete allows for seamlessly transferring the selected item from the renderInput to the InputProps of the textfield component

Exploring the functionality of the updated version of Autocomplete, my goal is to ensure that when an element is chosen from the dropdown menu, the input format of the text field will be modified to accommodate adding a chip with the selected text. Is the ...

After unraveling the unicode in JavaScript, the result often comes out as

Within my .Json file, I have two different unicodes that appear like this: \u30a2\u30eb\u30d0 \u0410\u043d\u0433\u0438\u043b\u044c\u044f When the Javascript code accesses the .Json file using const data = ...

Eliminate any null strings from an object by comparing the object's previous state with its updated state

I am currently implementing an exemptions scheduler using react-multi-date-picker. The react-multi-date-picker component is integrated within a react-table in the following manner: const [data, setData] = useState(0); const [dateValues, setDateValues] = us ...

One controller displays ng-repeats while the other does not

I have 2 controllers loading in different locations within my view. One controller works perfectly, but the other one does not show ng-repeats or appear in ng-inspector. I have confirmed that the http data is visible in the inspector. Both controllers are ...

How to efficiently filter objects within a child array in an ng-repeat statement, utilizing track by $index to manage duplicates

I have a variable called narrow.searchedMenu that contains 3 arrays of equal length. To display them, I am using ng-repeat with track by $index due to some duplicate elements present. <ul> <li ng-repeat="item in narrow.searchedMenu.desc ...

serving files using express.static

I have set up express.static to serve multiple static files: app.use("/assets", express.static(process.cwd() + "/build/assets")); Most of the time, it works as expected. However, in certain cases (especially when downloading many files at once), some fil ...

Accessing a webpage by directly pasting the URL into the address bar is functioning properly, but is

I'm facing an issue with accessing files from a vendor's application. When I directly enter the endpoints into the browser's address bar, the file opens up without any problems. However, when I try to access them using jQuery AJAX, I receive ...

Implementing a gradient effect on a specific image element within an HTML canvas with the help of jQuery

Currently, I'm working on an HTML canvas project where users can drop images onto the canvas. I am looking to implement a linear gradient effect specifically on the selected portion of the image. My goal is to allow users to use their mouse to select ...

Creating an array in C# to store a variable number of values

Is there a way to stop adding values to an array when I hit ENTER, rather than a specific number? Currently, I have this condition set up: while (numbers[i] != 10) { i++; numbers[i] = int.Parse(Console.ReadLine()); Console.WriteLine(numbers[i ...