0

I'm adding a class after an event in knockout, and i can't seem to figure out how to get the class off of the div when clicking on the rest of the page. Here's my code. I tried a few things to no avail. I want to remove the "open" class on .nav-menu.cart when you click anywhere else on the page. Thanks

self.addProductToCart = function(data) {
 var $productNotification = $(".product-notification");
 ax.Cart.addCartItem({product_id:data.id, name:data.name, description:data.description});
 $productNotification.slideDown(1000).fadeOut(200);
 $(".nav-menu.cart").addClass("open");

};

erics15
  • 567
  • 1
  • 7
  • 16

1 Answers1

1

Whenever you need to alter the DOM, you should try to do so via a knockout data-bind. Most of it can be done through knockout's default bindings; some stuff will require a custom binding.

For the first part: adding an open class should happen through the css bind:

data-bind="css: { 'classname' : state }"

In your viewmodel, add an cartIsOpen observable and an openCart method.

self.isOpen = ko.observable(false);

Now, you can open your cart visually by adding this data-bind to its container element:

data-bind="css: {'open': isOpen }"

The jquery line can be replaced by:

self.isOpen(true);

and knockout will take care of the css.

Now, for the (more complicated) second part:

Listening to an event outside a container is not something knockout's regular click or event binding can do. So you'll have to write a custom binding.

Listening to events outside a certain element can be quite tricky; I'd suggest searching stack overflow for some general advice. You'll have to be really careful with event.stopPropagation() for it to work.

In the example below I've coded a crude example of a clickOutside binding. Note that it's not a "finished" example:

  • Any click in your page will trigger the closeOnClickOutside method; you need to find a way to attach the listener when isOpen is set to true, and dispose it once it's set to false.
  • Whenever you want to open your menu you'll need to explicitly stop the click event from reaching the document. (cancelBubble in the example). Otherwise, the menu is opened and closed instantly.

var elementContainsChild = function(parent, child) {
  // http://stackoverflow.com/a/2234986/3297291
  var node = child.parentNode;
  while (node !== null) {
    if (node === parent) {
      return true;
    }
    node = node.parentNode;
  }
  return false;
};

ko.bindingHandlers.clickOutside = {
  init: function(element, valueAccessor) {
    var cb = valueAccessor();
    var closeOnClickOutside = function(e) {
      if (e.target !== element && 
          !elementContainsChild(element, e.target)) {
          cb();
      }
    };
    
    document.addEventListener("click", closeOnClickOutside);
  }
}

var vm = {
  isOpen: ko.observable(false)
};

ko.applyBindings(vm);
html, body { height: 100%; background: grey; }
.menu { background: yellow; display: none; }
.menu.open { display: block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<button data-bind="click: isOpen.bind(null, true), clickBubble: false">open menu</button>
<div class="menu" data-bind="css: {'open' : isOpen }, clickOutside: isOpen.bind(null, false)">
  <ul>
    <li>Menu item</li>
    <li>Menu item</li>
    <li>Menu item</li>
    <li>Menu item</li>
  </ul>
</div>
user3297291
  • 22,592
  • 4
  • 29
  • 45
  • I appreciate that, wow. I was going about it all sorts of wrong. I will try what you said. Very helpful – erics15 Jul 26 '16 at 21:23
  • One more question - how would I chain that to the button which already looks like this.? ? thanks! – erics15 Jul 27 '16 at 16:59
  • 1
    If the `addProductToCart` should open the menu (`self.isOpen(true)`), you'll need to add the `clickBubble: false` data-bind to the button as well. If you don't, the click event on the button will (1) open the menu, (2) bubble up all the way to document where it (3) closes the menu again (assuming you haven't yet changed the custom binding). – user3297291 Jul 27 '16 at 17:47