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");
}
}
...