Search code examples
javascriptvue.jsevent-handlingdom-eventsvue-events

Vue stopPropagation not working - child to parent


I am struggling with events propagation. The task is to prevent clicking on anything alse, if the data is not saved. So, The left div contains a tree of options. After clicking on an item, a setting in the right div is showed. I want to warn the user, when he tries to click outside the settings div, that the data is not saved.

The code looks like this:

   <div class="row">
       <div class="col-sm-6">
           <tree
               :data="treeData">
               .. some tree data
           </tree>
       </div>
       <div class="col-sm-6">
               <div class="treeOptionEdit" v-if="showEditBox" v-click-outside.stop="checkIfSaved">

           ... setting input fields

vue-outside-events package is used to detect clicks outside div. It works fine. The problem is, that this code, doesn't do anything:

        checkIfSaved : function (event, el)
        {
            event.stopPropagation();
            event.preventDefault();
        },

The click gets propagated to the parent anyway. If I put an event @click to the parent, it is fired an all clicks.

Does stop propagation work only from parent to children?


Solution

  • I'm not familiar with the v-click-outside implementation, but "click outside" events are typically implemented by using event delegation on the document/body element, and so by that time the event has already propagated and cannot be stopped.

    Here's a small example of what's going on:

    Vue.directive('click-outside', {
      bind(el, { value }) {
        el._handler = e => {
          if (!el.contains(e.target)) {
            value(e);
          }
        };
        
        document.addEventListener('click', el._handler);
      },
      
      unbind(el) {
        document.removeEventListener('click', el._handler);
      },
    });
    
    new Vue({
      el: '#app',
      methods: {
        onClick(e, color) {
          alert(`Clicked the ${color} box.`);
        },
        
        onClickOutside(e, color) {
          e.stopPropagation();
          alert(`Clicked outside of ${color} box.`);
        },
      },
    });
    .box { padding: 20px; }
    .red { background-color: red; }
    .yellow { background-color: yellow; }
    .blue { background-color: blue; }
    <script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
    
    <div id="app">
      <div class="box red" @click="onClick($event, 'red')">
        <div class="box yellow" @click="onClick($event, 'yellow')">
          <div class="box blue" @click="onClick($event, 'blue')" v-click-outside="e => onClickOutside(e, 'blue')">
          </div>
        </div>
      </div>
    </div>

    1. Click the blue box. The blue box receives the click event first, then it bubbles up to the yellow then red boxes, as expected.
    2. Click the yellow box. The yellow then red boxes receive the click event, then once it bubbles to the document element (of which the click-outside directive has attached an event listener to) the click-outside handler is called. Calling stopPropagation() at this moment does nothing because the event has already bubbled to the top.

    I don't believe the .stop modifier does anything in your example because v-click-outside doesn't support this modifier (.stop is only supported by v-on; v-click-outside would need to support it).

    So to solve your problem you need to change the order of events: v-click-outside needs to be handled first before @click.

    You can achieve this by changing the v-click-outside implementation by using event capturing as follows:

    Vue.directive('click-outside', {
      bind(el, { value }) {
        el._handler = e => {
          if (!el.contains(e.target)) {
            value(e);
          }
        };
        
        // *** Using capturing ***
        document.addEventListener('click', el._handler, { capture: true });
      },
      
      unbind(el) {
        document.removeEventListener('click', el._handler, { capture: true });
      },
    });
    
    new Vue({
      el: '#app',
      methods: {
        onClick(e, color) {
          alert(`Clicked the ${color} box.`);
        },
        
        onClickOutside(e, color) {
          e.stopPropagation();
          alert(`Clicked outside of ${color} box.`);
        },
      },
    });
    .box { padding: 20px; }
    .red { background-color: red; }
    .yellow { background-color: yellow; }
    .blue { background-color: blue; }
    <script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
    
    <div id="app">
      <div class="box red" @click="onClick($event, 'red')">
        <div class="box yellow" @click="onClick($event, 'yellow')">
          <div class="box blue" @click="onClick($event, 'blue')" v-click-outside="e => onClickOutside(e, 'blue')">
          </div>
        </div>
      </div>
    </div>

    Why not submit a pull request to vue-outside-events to support this feature?