6
\$\begingroup\$

A web page has a nested list. The goal is to insert the number of descendants for each li node (except for leaf nodes).

Here is an example of the expected output:

Animals [9]
    Mammals [4]
        Cows
        Donkeys
        Dogs
        Tigers
    Other [3]
        Snakes
        Birds
        Lizards
Fishes [5]
    Aquarium [2]
        Guppy
        Angelfish
    Sea [1]
        Sea trout

Here is the javascript I wrote:

function isLeaf(elem) {
    if(elem.firstElementChild === null) {
        return true;
    }

    else {
        return false;
    }
}

function countDescendants(elem) {
    if(isLeaf(elem)) return 0;

    else {
        let children = elem.firstElementChild.children;
        let count = 0;

        for(let child of children) {
            if(isLeaf(child)) {
                   count++;
            }

            else {
                count = count + 1 + countDescendants(child);
            }

        }
        return count;
    }
}

function insertCount(elem) {
    let count = countDescendants(elem);
    let text = elem.firstChild.textContent.trim();
    text = text + " [" + count + "]";
    elem.firstChild.textContent = text;
}

function traverseDOM(root) {
    let children = root.children;

    for(let child of children) {
        if(!isLeaf(child)) {
            if(child.tagName === "LI") {
                insertCount(child);
            }
            traverseDOM(child);
        }
    }
}

//traverse DOM tree
//try catch is for debugging only
try {
    if(document.body.firstElementChild !== null) {
        traverseDOM(document.body.firstElementChild);
    }
}  catch (err) {
    console.log(err);
}

The code works. I get this nagging feeling it could be done better. For example, is there a way to improve the countDescendants function?

\$\endgroup\$
0
1
\$\begingroup\$

I think functional coding style is best suited here, because it will significantly reduce amount of code. Also, you will have clearly separated structure consisting of functions with single responsibility.

Suggested solution

function insertCounts(root) {
    forEachElement(root, (el) => el.innerHTML = countText(count(el)) + el.innerHTML, el => !isLeaf(el) && (el.tagName === 'LI'));
}

function countText(count) {
     return `[${count}]`;
}

function forEachElement(root, apply, predicate = () => true) {
    Array.from(root.children).forEach(el => { predicate(el) && apply(el); forEachElement(el, apply, predicate) });
}

function count(root) {
    return Array.from(root.children).reduce((acc, el) => isLeaf(el) ? acc + 1 : acc + count(el), 0);
}

function isLeaf(elem) {
    return (elem.firstElementChild === null);
}


try {
    if(document.body.firstElementChild !== null) {
        insertCounts(document.body.firstElementChild);
    }
}  catch (err) {
    console.log(err);
}
\$\endgroup\$
4
  • \$\begingroup\$ Just wow. This solution shows the difference between a pro and a novice. Using Array.from is clever. I absolutely agree that each function should only do one task. Couple of questions though. In the forEachElement function why have predicate = () => true? I mean if I change it to predicate I get the same output. \$\endgroup\$
    – Mr Wannabe
    Jun 17 '18 at 1:32
  • \$\begingroup\$ Also I want to display the count after the text and not before. I changed the insertCount function to this: function insertCounts(root) { forEachElement(root, el => el.firstChild.textContent = el.firstChild.textContent.trim() + countText(count(el)), el => !isLeaf(el) && (el.tagName === 'LI')); } It works but is there a better way? \$\endgroup\$
    – Mr Wannabe
    Jun 17 '18 at 1:32
  • \$\begingroup\$ predicate = () => true: this is just default value for a parameter. I just thinking that usual call of this function will be with two arguments, that's why I made default third one. In this particular case we have only one call of function forEachElement() and we pass third argument, so default value is redundant. \$\endgroup\$
    – Stexxe
    Jun 18 '18 at 9:09
  • \$\begingroup\$ Unfortunately, I don't see better way to change text content than yours. \$\endgroup\$
    – Stexxe
    Jun 18 '18 at 9:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.