Getting only one element of the array and its children

Viewed 238

I have an multidimensional array categories and I am looping over this array with menuToElement function.

It takes al the elements of the array and its children and returns it in a ul. the code is fully working, but what I can't figure out is how to loop over only one element of the array and all its children.

What I want to do is for example only loop over the engine element and all its children and all the children arrays. My question is: Is this even possible with the for...of loop that I am using, because I tried looping over the index of categories and it says it is not iterable.

function menuToElement(menu) {
  const ul = document.createElement("ul");
  for (const item of menu) {
    const li = document.createElement("li");
    if (Object(item) === item) {
      li.textContent = item.text + ' \u25BD';
      li.appendChild(menuToElement(item.children));
    } else {
      li.textContent = item;
    }
    ul.appendChild(li);
  }
  return ul;
}

var categories = [{
  text: "engine",
  children: [1, 2, 3, {
    text: "piston",
    children: [4, 5, 6, {
      text: "piston",
      children: [4, 5, 6]
    }]
  }]
}, {
  text: "tire",
  children: [7, 8, 9]
}];
const ul = menuToElement(categories);
document.getElementById("menu").appendChild(ul);
li>ul {
  display: none;
}

li:hover>ul {
  display: block;
}
<div id="menu"></div>

2 Answers

First we start with some helpers that make our life easier: elem and text -

const elem = (tag, ...children) =>
{ const node = document.createElement(tag)
  children.forEach(node.appendChild.bind(node))
  return node
}

const text =
  document.createTextNode.bind(document)

const doc =
  elem
    ( "article"                    // <article>
    , elem("p", text("wash hand")) //   <p>wash hand</p>
    , elem("p", text("read book")) //   <p>read book</p>
    , elem("p", text("stay safe")) //   <p>stay safe</p>
    )                              // </article>

document.body.appendChild(doc)
article { border: 1px solid tomato; }
p { border: 1px solid dodgerblue; }

Next we define how to convert one menu item to a menu node, toMenu1 -

  1. if the input item is an object, return a LI with item.text and call toMenu on item.children
  2. otherwise (by induction) the input item is not an object. In this case, simply return an LI with plain text
const toMenu1 = (item = {}) =>
  Object(item) === item
    ? elem('li', text(`${item.text} \u25BD`), toMenu(item.children)) // 1
    : elem('li', text(item))                                         // 2

We haven't written toMenu yet, but above we see that it expects an array of items. Writing this function is even easier than the last -

  1. Return a UL where each item is converted using toMenu1
const toMenu = (all = []) =>
  elem('ul', ...all.map(toMenu1)) // 1

Before we continue, expand the snippet below to verify the results in your own browser -

const elem = (tag, ...children) =>
{ const node = document.createElement(tag)
  children.forEach(node.appendChild.bind(node))
  return node
}

const text =
  document.createTextNode.bind(document)

const toMenu1 = (item = {}) =>
  Object(item) === item
    ? elem('li', text(`${item.text} \u25BD`), toMenu(item.children))
    : elem('li', text(item))
    
const toMenu = (all = []) =>
  elem('ul', ...all.map(toMenu1))

var categories = [{
  text: "engine",
  children: [1, 2, 3, {
    text: "piston",
    children: [4, 5, 6, {
      text: "piston",
      children: [4, 5, 6]
    }]
  }]
}, {
  text: "tire",
  children: [7, 8, 9]
}];

const ul = toMenu(categories)
document.getElementById("menu").appendChild(ul)
li>ul {
  display: none;
}

li:hover>ul {
  display: block;
}
<div id="menu"></div>


Now to answer your specific question: Can we build a recursive menu using just one input item?

You can do this one of two ways. 1) By grabbing just the item you care about -

const ul = toMenu([ categories[0] ]) // just the first category
// <ul>
//   <li>engine<ul>...</ul></li>
// </ul>

Or 2) By using Array.prototype.filter -

const ul = toMenu(categories.filter(c => c.text === "engine"))
// <ul>
//   <li>engine<ul>...</ul></li>
// </ul>

Bonus: the special relationship between toMenu1 and toMenu is known as mutual recursion. toMenu1 calls toMenu, which calls toMenu1, which calls toMenu, and so on...

Related