2
\$\begingroup\$

How can I improve this component with using event delegation rather than doing a lookup using parentNode so that I can change the markup around without having to update the JavaScript?

HTML

  <div class="drop-down-wrapper">

    <div class="drop-down-btn-wrapper">
      <div class="drop-down-open"><img src="http://placehold.it/175x35" /></div>
      <div class="drop-down-closed"><img src="http://placehold.it/150x35" /></div>
    </div>

    <div class="drop-down-content js-drop-down">
      <div class="content-wrapper">
        <p>
          Lorem ipsum dolor sit amet, consectetuer adipiscing elit
        </p>
      </div>
    </div>

  </div>  

JavaScript

(function(){

      var dropDownClosed = 'js-drop-down',
          dropDownButtonList = document.querySelectorAll('.drop-down-btn-wrapper');

      function init() {
        for(var i=0; i<dropDownButtonList.length; i++) {
          dropDownButtonList[i].addEventListener('click', initDropdown, false);
        }
      }

      function initDropdown(e) {

        var event          = e.target;
        var contentWrapper = event.parentNode.parentNode.parentNode.childNodes[3];
        var content        = event.parentNode.parentNode.parentNode.childNodes[3].childNodes[1];
        var closedButton   = event.parentNode.parentNode.parentNode.childNodes[1].childNodes[3];
        var contentHeight  = content.offsetHeight;

        if(hasClass(contentWrapper, dropDownClosed)) {
          removeClass(contentWrapper, dropDownClosed);
          contentWrapper.style.height = contentHeight+'px';
          closedButton.style.opacity = '0.0';
        } else {
          addClass(contentWrapper, dropDownClosed);
          contentWrapper.style.height = 0+'px';
          closedButton.style.opacity = '1.0';
        }
      }

      function hasClass(ele, cls) {
          return ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));
      }
      function addClass(ele, cls) {
          if (!hasClass(ele, cls)) ele.className += " " + cls;
      }
      function removeClass(ele, cls) {
          if (hasClass(ele, cls)) {
              var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');
              ele.className = ele.className.replace(reg, '');
          }
      }

      init();  


    }());

CSS

.drop-down-wrapper {
  position:relative;
  border: 2px black solid;
  margin-bottom:20px;
  padding:20px 0;
}

.content-wrapper {
  padding:15px;
  margin:30px 0;
}

.drop-down-open {
  position:absolute;
  top:0;
  border: thin red solid;
}

.drop-down-closed {
  position:absolute;
  top:0;
  border: 2px solid green;
}

.drop-down-content {
  background-color:#fff;
  overflow:hidden;
  height:0;
}

DEMO

\$\endgroup\$
2
  • \$\begingroup\$ Can you describe the purpose of this component? What am I supposed to click on, and what should I expect to happen? I want to make sure I'm understanding it correctly before I provide a review. \$\endgroup\$
    – Thriggle
    May 15, 2015 at 19:26
  • \$\begingroup\$ Take a look at the demo, when you click the image it reveals hidden content. \$\endgroup\$
    – zadubz
    May 16, 2015 at 16:12

1 Answer 1

1
\$\begingroup\$

Sure, you can use event delegation for this. For the uninitiated, event delegation in JavaScript means attaching the event listener to a container element and using the target property of the event to identify which sub-element was acted upon.

You'll still need to crawl your way up the DOM tree to get to the root element, which can be done without knowing the DOM structure as long as you have a sentinel you can check, such as the root element's class name, to determine whether you've reached the root.

Once you've made it to the top, so to speak, you can use query selectors to find your way back down to specific child nodes within the root element, again allowing you to grab or modify specific elements without knowing the HTML structure--all you'll need to know is a reliably query-selectable combination of attributes (such as an element class).

Here's a working example with comments:

(function() {
  var dropDownClosed = "js-drop-down",
    dropDownButtonList = document.querySelectorAll(".drop-down-wrapper");

  function init() {
    for (var i = 0; i < dropDownButtonList.length; i++) {
      dropDownButtonList[i].addEventListener('click', initDropdown, false);
    }
  }

  function initDropdown(e) {
    e = e || window.event;
    var target = e.target || e.srcElement,
      validElement = false;
    while (!hasClass(target, "drop-down-wrapper")) { /* crawl up the DOM until reaching "root" element */
      target = target.parentNode;
      if (!validElement) { /* track whether user clicked on the open or close buttons */
        if (hasClass(target, "drop-down-open") || hasClass(target, "drop-down-closed")) {
          validElement = true;
        }
      }
    }
    if (validElement) { /* Only toggle content if open/close button was clicked. */
      /* Get the desired content based on its attributes rather than its DOM position: */
      var contentWrapper = target.querySelector(".drop-down-content");
      var content = contentWrapper.querySelector("*");
      var closedButton = target.querySelector(".drop-down-closed");
      var contentHeight = content.offsetHeight;
      if (hasClass(contentWrapper, dropDownClosed)) {
        removeClass(contentWrapper, dropDownClosed);
        contentWrapper.style.height = contentHeight + 'px';
        closedButton.style.opacity = '0.0';
      } else {
        addClass(contentWrapper, dropDownClosed);
        contentWrapper.style.height = 0 + 'px';
        closedButton.style.opacity = '1.0';
      }
    }
  }

  function hasClass(ele, cls) {
    return ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));
  }

  function addClass(ele, cls) {
    if (!hasClass(ele, cls)) ele.className += " " + cls;
  }

  function removeClass(ele, cls) {
    if (hasClass(ele, cls)) {
      var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');
      ele.className = ele.className.replace(reg, '');
    }
  }

  init();


}());
.drop-down-wrapper {
  position: relative;
  border: 2px black solid;
  margin-bottom: 20px;
  padding: 20px 0;
}
.content-wrapper {
  padding: 15px;
  margin: 30px 0;
}
.drop-down-open {
  position: absolute;
  top: 0;
  border: thin red solid;
}
.drop-down-closed {
  position: absolute;
  top: 0;
  border: 2px solid green;
}
.drop-down-content {
  background-color: #fff;
  overflow: hidden;
  height: 0;
}
<div class="drop-down-wrapper">

  <div class="drop-down-btn-wrapper">
    <div class="drop-down-open">
      <img src="http://placehold.it/175x35" />
    </div>
    <div class="drop-down-closed">
      <img src="http://placehold.it/150x35" />
    </div>
  </div>

  <div class="drop-down-content js-drop-down">
    <div class="content-wrapper">
      <p>
        Lorem ipsum dolor sit amet, consectetuer adipiscing elit
      </p>
    </div>
  </div>

</div>
<div class="drop-down-wrapper">

  <div class="drop-down-btn-wrapper">
    <div class="drop-down-open">
      <img src="http://placehold.it/175x35" />
    </div>
    <div class="drop-down-closed">
      <img src="http://placehold.it/150x35" />
    </div>
  </div>

  <div class="drop-down-content js-drop-down">
    <div class="content-wrapper">
      <p>
        Lorem ipsum dolor sit amet, consectetuer adipiscing elit
      </p>
    </div>
  </div>

</div>

I did change your variable named event to target to make it more clear that it represents an element in the DOM tree that we're climbing.

You could improve this further by executing the relevant query selectors at or before the time the event listener is being attached, caching the results so that you don't have to requery the DOM every time an event is triggered.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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