KO binding: Menu

by whelkaholism

HTML

<nav data-bind="menu: {}">
  <button class="koa-nav-toggle" aria-expanded="false">Menu</button>

  <ul class="koa-menu">
    <li>
      <a href="#">Home</a>
    </li>

    <li class="has-submenu">
      <a href="#">Products</a>
      <ul class="koa-submenu">
        <li><a href="#">Phones</a></li>
        <li><a href="#">Laptops</a></li>

        <li class="has-submenu">
          <a href="#">Accessories</a>
          <ul class="koa-submenu">
            <li><a href="#">Chargers</a></li>
            <li><a href="#">Cases</a></li>
          </ul>
        </li>
      </ul>
    </li>

    <li><a href="#">Contact</a></li>
  </ul>
</nav>

CSS

/* Base reset */
.koa-nav,
.koa-menu,
.koa-submenu {
  list-style: none;
  margin: 0;
  padding: 0;
}

.koa-nav {
  background: #222;
  font-family: sans-serif;
}

.koa-menu a {
  display: block;
  padding: 12px 16px;
  color: white;
  text-decoration: none;
}

.koa-menu a:hover {
  background: #444;
}

/* Mobile first layout */
.koa-menu {
  display: none;
  flex-direction: column;
}

.koa-menu.open {
  display: flex;
}

.has-submenu > a::after {
  content: " ▾";
  font-size: 0.8em;
}

/* Submenus */
.koa-submenu {
  display: none;
  background: #333;
}

.has-submenu.open > .koa-submenu {
  display: block;
}

/* Toggle button */
.koa-nav-toggle {
  background: #222;
  color: white;
  border: none;
  padding: 12px 16px;
  font-size: 16px;
  width: 100%;
  text-align: left;
  cursor: pointer;
}

.koa-submenu {
  transition: max-height 0.3s ease;
}

/* Desktop styles */
@media (min-width: 768px) {
  .koa-nav-toggle {
    display: none;
  }

  .koa-menu {
    display: flex;
    flex-direction: row;
  }

  .koa-menu > li {
    position: relative;
  }

  .koa-submenu {
    position: absolute;
    left: 0;
    top: 100%;
    min-width: 200px;
  }

  .koa-submenu .koa-submenu {
    left: 100%;
    top: 0;
  }
}

JavaScript

(function()
{
    // ------------------------------------------------------------------------------------------------------------------------------------------------
    // SIMPLE MENU
    ko.bindingHandlers.menu = {
        init:
            function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
            {
                var obs = f_valueaccessor();

                if (!!ko.unwrap(obs))
                {
                    el.classList.add('koa-nav');
                    const navtoggle = el.querySelector(".koa-nav-toggle");
                    const menu = el.querySelector(".koa-menu");

                    navtoggle.addEventListener("click", () =>
                    {
                        const isopen = menu.classList.toggle("open");
                        navtoggle.setAttribute("aria-expanded", isopen);
                    });

                    // Submenu toggles

                    el.querySelectorAll(".koa-menu li.has-submenu > a").forEach(link =>
                    {
                        link.addEventListener("click", (e) =>
                        {                          
                            const parent = link.parentElement;

                            // Only intercept click if submenu exists
                            if (parent.classList.contains("has-submenu"))
                            {
                                e.preventDefault();

                                // Close sibling submenus
                                const siblings = parent.parentElement.children;
                                for (let sib of siblings)
                                {
                                    if (sib !== parent)
                                    {
                                        sib.classList.remove("open");
                                    }
                                }

                               ...