skip to Main Content

I am using react list item to display list of sidebar menu based on role I want to apply styles for the same I am trying to get element by tag name but I am getting empty list from that .
Here is the code :

      <ul className="metismenu list-unstyled" ref={refs} id="side-menu">

           {lists.map((item, index) => {
                  return (
                        <li key={index}>
                            <Link to={item.link} className={item.submenu ? 'has-arrow' : ''}>
                                <FeatherIcon icon={item.icon} />
                                   <span>{item.name}</span>
                            </Link>
                           {item.submenu ? <SubMenuList menu={item.submenu} /> : ''}
                         </li>
                        );
                    })}
      </ul>



        const initMenu = () => {
        new MetisMenu("#side-menu");
        let matchingMenuItem = null;
        const ul = document.getElementById("side-menu");
        console.log(ul);
        const childNodes = ul.childNodes;
        console.log(childNodes);
        console.log(refs.current);
        const items = refs.current.getElementsByTagName("a");
        console.log(items);
        for (let i = 0; i < items.length; ++i) {
            if (pathName === items[i].pathname) {
                matchingMenuItem = items[i];
                break;
            }
        }
        console.log(matchingMenuItem);
        if (matchingMenuItem) {
            activateParentDropdown(matchingMenuItem);
        }
    };

Heres the result:-
enter image description here

I am getting empty list for anchor tag.

Edit

How I am activating dropdown for each element based on router url

const activateParentDropdown = useCallback((item) => {
    console.log(item);
    item.classList.add("active");
    const parent = item.parentElement;
    console.log(parent);
    const parent2El = parent.childNodes[1];
    if (parent2El && parent2El.id !== "side-menu") {
        parent2El.classList.add("mm-show");
    }

    if (parent) {
        parent.classList.add("mm-active");
        const parent2 = parent.parentElement;

        if (parent2) {
            parent2.classList.add("mm-show"); // ul tag

            const parent3 = parent2.parentElement; // li tag

            if (parent3) {
                parent3.classList.add("mm-active"); // li
                parent3.childNodes[0].classList.add("mm-active"); //a
                const parent4 = parent3.parentElement; // ul
                if (parent4) {
                    parent4.classList.add("mm-show"); // ul
                    const parent5 = parent4.parentElement;
                    if (parent5) {
                        parent5.classList.add("mm-show"); // li
                        parent5.childNodes[0].classList.add("mm-active"); // a tag
                    }
                }
            }
        }
        scrollElement(item);
        return false;
    }
    scrollElement(item);
    return false;
}, []);

2

Answers


  1. Note that if the code is run directly after returning your jsx it hasn’t had time to get to the DOM yet. React updates the DOM asynchronously after having made changes to its virtual DOM…

    But: The console.log sometimes fills in values of object after they have been consoled.log, which is probably what is happening for your console.log of refs.current… So at log time it is actually an empty NodeList… but the log get ‘updated’ after the fact…

    So if you changed your code from the line const items to

    const thingsToDo = () => {
      const items = refs.current.getElementsByTagName("a");
      if (items.length === 0) {
        setTimeout(thingsToDo, 10);
        return;
      }
      console.log(items);
      for (let i = 0; i < items.length; ++i) {
        if (pathName === items[i].pathname) {
          matchingMenuItem = items[i];
          break;
        }
      }
      console.log(matchingMenuItem);
      if (matchingMenuItem) {
        activateParentDropdown(matchingMenuItem);
      }
    }
    thingsToDo();
    

    thus waiting for the elements to exist, things would probably work, although this seems like very ‘anti-patternish’ React-code – in React you normally use change the values of state variables to rerender a different state, mark a menu choice as active, fold out a submenu etc, not raw DOM parsing and manipulation. But that’s up to you to decide.

    Hopefully this code solves the immediate problem at hand 🙂

    Login or Signup to reply.
  2. Answer to Chethu:s additional question:

    Ok, we are now far from React best practices, but it seems like your submenus are ‘always’ in the DOM – but being shown by adding the class "mm-show".

    So add a delegated event handler that will only be set ONCE in your code make sure it not added twice (maybe inside a useEffect in your main component (App?). When someone clicks the body, check if the click is inside a shown sub menu and then remove the class ‘mm-show’ from it…

    useEffect(() => {
      document.body.addEventListener('click', e => {
        let shownSubMenu = e.target.closest('.mm-show');
        if (!shownSubMenu) { return; }
        shownSubMenu.classList.remove('mm-show');
        // maybe also remove all mm-active classes from elements
        [...document.querySelectorAll('.mm-active')].forEach(x =>
          x.classList.remove('mm-active'));
      });
    }, []);
    

    Potentially you always want to hide a submenu after the user clicks anywhere, even outside it, if so then do this instead:

    useEffect(() => {
      document.body.addEventListener('click', e => {
        let shownSubMenu = document.querySelector('.mm-show');
        if (!shownSubMenu) { return; }
        shownSubMenu.classList.remove('mm-show');
        // maybe also remove all mm-active classes from elements
        [...document.querySelectorAll('.mm-active')].forEach(x =>
          x.classList.remove('mm-active'))
      })
    }, []);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search