Detectar clic en elemento exterior

121

¿Cómo puedo detectar un clic fuera de mi elemento? Estoy usando Vue.js, por lo que estará fuera de mi elemento de plantillas. Sé cómo hacerlo en Vanilla JS, pero no estoy seguro de si hay una forma más adecuada de hacerlo, cuando estoy usando Vue.js.

Esta es la solución para Vanilla JS: evento Javascript Detect Click fuera de div

¿Supongo que puedo usar una mejor manera de acceder al elemento?

Comunidad
fuente
Los componentes de Vue están aislados. por lo que la detección de cambios externos está fuera de discusión y se usa anti patrón.
Raj Kamal
Gracias. Sin embargo, no estoy seguro de cómo implementarlo en un componente de Vue. ¿Todavía debe haber algunas mejores prácticas para el anti-patrón?
Los componentes de Vue.js están aislados, eso es cierto, pero existen diferentes métodos para la comunicación entre padres e hijos. Entonces, en lugar de pedir que se detecte un evento fuera de un elemento, debe especificar si desea detectar elementos dentro de un componente, desde el componente principal, desde algún hijo o cualquier relación entre componentes
Yerko Palma
Gracias por la respuesta. ¿Tiene algunos ejemplos o enlaces que pueda seguir?
github.com/simplesmiler/vue-clickaway puede simplificar su trabajo
Raj Kamal

Respuestas:

97

Se puede resolver muy bien configurando una directiva personalizada una vez:

Vue.directive('click-outside', {
  bind () {
      this.event = event => this.vm.$emit(this.expression, event)
      this.el.addEventListener('click', this.stopProp)
      document.body.addEventListener('click', this.event)
  },   
  unbind() {
    this.el.removeEventListener('click', this.stopProp)
    document.body.removeEventListener('click', this.event)
  },

  stopProp(event) { event.stopPropagation() }
})

Uso:

<div v-click-outside="nameOfCustomEventToCall">
  Some content
</div>

En el componente:

events: {
  nameOfCustomEventToCall: function (event) {
    // do something - probably hide the dropdown menu / modal etc.
  }
}

Demostración de trabajo en JSFiddle con información adicional sobre advertencias:

https://jsfiddle.net/Linusborg/yzm8t8jq/

Linus Borg
fuente
3
Usé el vue clickaway, pero creo que su solución es más o menos la misma. Gracias.
56
Este enfoque ya no funciona en Vue.js 2. La llamada self.vm. $ emit da un mensaje de error.
Northernman
3
Usar @blur también es una opción y hace que sea más fácil dar el mismo resultado: <input @ blur = "hide"> donde hide: function () {this.isActive = false; }
Craws
1
La respuesta debe editarse para indicar que es solo para Vue.js 1
Stéphane Gerber
167

Existe la solución que utilicé, que se basa en la respuesta de Linus Borg y funciona bien con vue.js 2.0.

Vue.directive('click-outside', {
  bind: function (el, binding, vnode) {
    el.clickOutsideEvent = function (event) {
      // here I check that click was outside the el and his children
      if (!(el == event.target || el.contains(event.target))) {
        // and if it did, call method provided in attribute value
        vnode.context[binding.expression](event);
      }
    };
    document.body.addEventListener('click', el.clickOutsideEvent)
  },
  unbind: function (el) {
    document.body.removeEventListener('click', el.clickOutsideEvent)
  },
});

Te unes a él usando v-click-outside:

<div v-click-outside="doStuff">

Aquí hay una pequeña demostración

Puede encontrar más información sobre directivas personalizadas y qué significa el, binding, vnode en https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments

MadisonTrash
fuente
8
Funcionó, pero en Vue 2.0 las directivas ya no tienen una instancia, por lo que esto no está definido. vuejs.org/v2/guide/migration.html#Custom-Directives-simplified . No tengo ni idea de por qué funciona este violín o cuándo se ha hecho esta simplificación. (Para resolver, reemplace "this" con "el" para vincular el evento al elemento)
Busata
1
Funciona probablemente porque la ventana pasó como "esto". He arreglado la respuesta. Gracias por señalar este error.
MadisonTrash
8
¿Hay alguna forma de excluir un elemento específico externo? Por ejemplo, tengo un botón afuera que tiene que abrir este elemento y debido a que activa ambos métodos no sucede nada.
Žilvinas
5
¿Puede explicar vnode.context [binding.expression] (evento); ?
Sainath SR
1
¿Cómo cambiar esto para que se pueda usar una expresión en lugar de que se active un método dentro del v-click-outside?
raphadko
50

Agregue un tabindexatributo a su componente para que pueda enfocarse y haga lo siguiente:

<template>
    <div
        @focus="handleFocus"
        @focusout="handleFocusOut"
        tabindex="0"
    >
      SOME CONTENT HERE
    </div>
</template>

<script>
export default {    
    methods: {
        handleFocus() {
            // do something here
        },
        handleFocusOut() {
            // do something here
        }
    }
}
</script>
G'ofur N
fuente
4
¡Guau! Encuentro esta como la solución más corta y limpia. También el único que funcionó en mi caso.
Matt Komarnicki
3
Solo para agregar a esto, establecer un tabindex de -1 evitará que aparezca el cuadro resaltado cuando haga clic en el elemento, pero aún permitirá que el div sea enfocable.
Colin
1
Por alguna razón, tabindex de -1 no oculta el contorno para mí, así que solo agregué outline: none;el foco para el elemento.
Art3mix
1
¿Cómo podemos aplicar esto a un navegador lateral fuera del lienzo que se desliza en la pantalla? No puedo dar el enfoque de navegación lateral a menos que se haga clic en él,
Charles Okwuagwu
1
Esta es la forma más poderosa absolutamente. ¡Gracias! :)
Canet Robern
23

Hay dos paquetes disponibles en la comunidad para esta tarea (ambos se mantienen):

Julien Le Coupanec
fuente
8
vue-clickawayEl paquete resolvió mi problema perfectamente. Gracias
Abdalla Arbab
1
¿Qué pasa con muchos artículos? Cada elemento con un evento de clic externo activará un evento en cada clic. Es bueno cuando haces un diálogo y terrible cuando creas una galería. En la era sin componentes, estamos escuchando el clic del documento y verificamos en qué elemento se hizo clic. Pero ahora es un dolor.
br.
@Julien Le Coupanec ¡He encontrado esta solución la mejor con diferencia! ¡Muchas gracias por compartirlo!
Manuel Abascal
7

Esto funcionó para mí con Vue.js 2.5.2:

/**
 * Call a function when a click is detected outside of the
 * current DOM node ( AND its children )
 *
 * Example :
 *
 * <template>
 *   <div v-click-outside="onClickOutside">Hello</div>
 * </template>
 *
 * <script>
 * import clickOutside from '../../../../directives/clickOutside'
 * export default {
 *   directives: {
 *     clickOutside
 *   },
 *   data () {
 *     return {
         showDatePicker: false
 *     }
 *   },
 *   methods: {
 *     onClickOutside (event) {
 *       this.showDatePicker = false
 *     }
 *   }
 * }
 * </script>
 */
export default {
  bind: function (el, binding, vNode) {
    el.__vueClickOutside__ = event => {
      if (!el.contains(event.target)) {
        // call method provided in v-click-outside value
        vNode.context[binding.expression](event)
        event.stopPropagation()
      }
    }
    document.body.addEventListener('click', el.__vueClickOutside__)
  },
  unbind: function (el, binding, vNode) {
    // Remove Event Listeners
    document.removeEventListener('click', el.__vueClickOutside__)
    el.__vueClickOutside__ = null
  }
}
yann_yinn
fuente
Gracias por este ejemplo. Comprobé esto en vue 2.6. Hay alguna solución, en el método de desvinculación debe solucionar algún problema con esto (olvidó la propiedad del cuerpo en el método de desvinculación): document.body.removeEventListener ('click', el .__ vueClickOutside__); si no, causará la creación de múltiples oyentes de eventos después de cada recreación de componentes (actualización de página);
Alexey Shabramov
7
export default {
  bind: function (el, binding, vNode) {
    // Provided expression must evaluate to a function.
    if (typeof binding.value !== 'function') {
      const compName = vNode.context.name
      let warn = `[Vue-click-outside:] provided expression '${binding.expression}' is not a function, but has to be`
      if (compName) { warn += `Found in component '${compName}'` }

      console.warn(warn)
    }
    // Define Handler and cache it on the element
    const bubble = binding.modifiers.bubble
    const handler = (e) => {
      if (bubble || (!el.contains(e.target) && el !== e.target)) {
        binding.value(e)
      }
    }
    el.__vueClickOutside__ = handler

    // add Event Listeners
    document.addEventListener('click', handler)
  },

  unbind: function (el, binding) {
    // Remove Event Listeners
    document.removeEventListener('click', el.__vueClickOutside__)
    el.__vueClickOutside__ = null

  }
}
xiaoyu2er
fuente
5

He combinado todas las respuestas (incluida una línea de vue-clickaway) y se me ocurrió esta solución que me funciona:

Vue.directive('click-outside', {
    bind(el, binding, vnode) {
        var vm = vnode.context;
        var callback = binding.value;

        el.clickOutsideEvent = function (event) {
            if (!(el == event.target || el.contains(event.target))) {
                return callback.call(vm, event);
            }
        };
        document.body.addEventListener('click', el.clickOutsideEvent);
    },
    unbind(el) {
        document.body.removeEventListener('click', el.clickOutsideEvent);
    }
});

Uso en componente:

<li v-click-outside="closeSearch">
  <!-- your component here -->
</li>
BogdanG
fuente
Más o menos lo mismo que la respuesta de @MadisonTrash a continuación
retrovertigo
3

He actualizado la respuesta de MadisonTrash para admitir Mobile Safari (que no tiene clickevento, touchenddebe usarse en su lugar). Esto también incorpora una verificación para que el evento no se active al arrastrar en dispositivos móviles.

Vue.directive('click-outside', {
    bind: function (el, binding, vnode) {
        el.eventSetDrag = function () {
            el.setAttribute('data-dragging', 'yes');
        }
        el.eventClearDrag = function () {
            el.removeAttribute('data-dragging');
        }
        el.eventOnClick = function (event) {
            var dragging = el.getAttribute('data-dragging');
            // Check that the click was outside the el and its children, and wasn't a drag
            if (!(el == event.target || el.contains(event.target)) && !dragging) {
                // call method provided in attribute value
                vnode.context[binding.expression](event);
            }
        };
        document.addEventListener('touchstart', el.eventClearDrag);
        document.addEventListener('touchmove', el.eventSetDrag);
        document.addEventListener('click', el.eventOnClick);
        document.addEventListener('touchend', el.eventOnClick);
    }, unbind: function (el) {
        document.removeEventListener('touchstart', el.eventClearDrag);
        document.removeEventListener('touchmove', el.eventSetDrag);
        document.removeEventListener('click', el.eventOnClick);
        document.removeEventListener('touchend', el.eventOnClick);
        el.removeAttribute('data-dragging');
    },
});
benrwb
fuente
3

Yo uso este código:

botón mostrar-ocultar

 <a @click.stop="visualSwitch()"> show hide </a>

elemento mostrar-ocultar

<div class="dialog-popup" v-if="visualState" @click.stop=""></div>

guión

data () { return {
    visualState: false,
}},
methods: {
    visualSwitch() {
        this.visualState = !this.visualState;
        if (this.visualState)
            document.addEventListener('click', this.visualState);
        else
            document.removeEventListener('click', this.visualState);
    },
},

Actualización: quitar el reloj; agregar detener la propagación

Pax Exterminatus
fuente
2

Odio las funciones adicionales, así que ... aquí hay una solución de vue increíble sin métodos de vue adicionales, solo var

  1. crear elemento html, establecer controles y directiva
    <p @click="popup = !popup" v-out="popup">

    <div v-if="popup">
       My awesome popup
    </div>
  1. crear una var en datos como
data:{
   popup: false,
}
  1. agregue la directiva vue. sus
Vue.directive('out', {

    bind: function (el, binding, vNode) {
        const handler = (e) => {
            if (!el.contains(e.target) && el !== e.target) {
                //and here is you toggle var. thats it
                vNode.context[binding.expression] = false
            }
        }
        el.out = handler
        document.addEventListener('click', handler)
    },

    unbind: function (el, binding) {
        document.removeEventListener('click', el.out)
        el.out = null
    }
})
Martin Prestone
fuente
2

Si busca específicamente un clic fuera del elemento pero aún dentro del elemento principal, puede usar

<div class="parent" @click.self="onParentClick">
  <div class="child"></div>
</div>

Yo uso esto para modales.

Andrés Holguin
fuente
1

Puede registrar dos detectores de eventos para eventos de clic como este

document.getElementById("some-area")
        .addEventListener("click", function(e){
        alert("You clicked on the area!");
        e.stopPropagation();// this will stop propagation of this event to upper level
     }
);

document.body.addEventListener("click", 
   function(e) {
           alert("You clicked outside the area!");
         }
);
saravanakumar
fuente
Gracias. Lo sé, pero parece que debe haber una mejor manera de hacer esto en Vue.js.
¡OKAY! deje que un genio de vue.js responda :)
saravanakumar
1
  <button 
    class="dropdown"
    @click.prevent="toggle"
    ref="toggle"
    :class="{'is-active': isActiveEl}"
  >
    Click me
  </button>

  data() {
   return {
     isActiveEl: false
   }
  }, 
  created() {
    window.addEventListener('click', this.close);
  },
  beforeDestroy() {
    window.removeEventListener('click', this.close);
  },
  methods: {
    toggle: function() {
      this.isActiveEl = !this.isActiveEl;
    },
    close(e) {
      if (!this.$refs.toggle.contains(e.target)) {
        this.isActiveEl = false;
      }
    },
  },
Dmytro Lishtvan
fuente
Gracias, funciona perfectamente y si solo lo necesita una vez, no hay necesidad de bibliotecas adicionales
Marian Klühspies
1

La respuesta corta: esto debe hacerse con directivas personalizadas .

Aquí hay muchas respuestas excelentes que también dicen esto, pero la mayoría de las respuestas que he visto se rompen cuando comienza a usar el clic externo de manera extensiva (especialmente en capas o con múltiples exclusiones). He escrito un artículo en el medio hablando sobre los matices de las Directivas personalizadas y específicamente la implementación de esta. Puede que no cubra todos los casos extremos, pero ha cubierto todo lo que se me ocurrió.

Esto tendrá en cuenta múltiples enlaces, múltiples niveles de exclusiones de otros elementos y permitirá que su controlador solo administre la "lógica comercial".

Aquí está el código para al menos la parte de la definición, consulte el artículo para obtener una explicación completa.

var handleOutsideClick={}
const OutsideClick = {
  // this directive is run on the bind and unbind hooks
  bind (el, binding, vnode) {
    // Define the function to be called on click, filter the excludes and call the handler
    handleOutsideClick[el.id] = e => {
      e.stopPropagation()
      // extract the handler and exclude from the binding value
      const { handler, exclude } = binding.value
      // set variable to keep track of if the clicked element is in the exclude list
      let clickedOnExcludedEl = false
      // if the target element has no classes, it won't be in the exclude list skip the check
      if (e.target._prevClass !== undefined) {
        // for each exclude name check if it matches any of the target element's classes
        for (const className of exclude) {
          clickedOnExcludedEl = e.target._prevClass.includes(className)
          if (clickedOnExcludedEl) {
            break // once we have found one match, stop looking
          }
        }
      }
      // don't call the handler if our directive element contains the target element
      // or if the element was in the exclude list
      if (!(el.contains(e.target) || clickedOnExcludedEl)) {
        handler()
      }
    }
    // Register our outsideClick handler on the click/touchstart listeners
    document.addEventListener('click', handleOutsideClick[el.id])
    document.addEventListener('touchstart', handleOutsideClick[el.id])
    document.onkeydown = e => {
      //this is an option but may not work right with multiple handlers
      if (e.keyCode === 27) {
        // TODO: there are minor issues when escape is clicked right after open keeping the old target
        handleOutsideClick[el.id](e)
      }
    }
  },
  unbind () {
    // If the element that has v-outside-click is removed, unbind it from listeners
    document.removeEventListener('click', handleOutsideClick[el.id])
    document.removeEventListener('touchstart', handleOutsideClick[el.id])
    document.onkeydown = null //Note that this may not work with multiple listeners
  }
}
export default OutsideClick
Marcus Smith
fuente
1

Lo hice de una manera ligeramente diferente usando una función dentro de created ().

  created() {
      window.addEventListener('click', (e) => {
        if (!this.$el.contains(e.target)){
          this.showMobileNav = false
        }
      })
  },

De esta manera, si alguien hace clic fuera del elemento, en mi caso, la navegación móvil está oculta.

¡Espero que esto ayude!

Casi Pitt
fuente
1

Ya hay muchas respuestas a esta pregunta, y la mayoría de ellas se basan en la idea de directiva personalizada similar. El problema con este enfoque es que uno tiene que pasar una función de método a la directiva y no puede escribir código directamente como en otros eventos.

Creé un nuevo paquete vue-on-clickoutque es diferente. Compruébalo en:

Le permite a uno escribir v-on:clickoutcomo cualquier otro evento. Por ejemplo, puedes escribir

<div v-on:clickout="myField=value" v-on:click="myField=otherValue">...</div>

y funciona.

Actualizar

vue-on-clickout ahora es compatible con Vue 3!

Mu-Tsun Tsai
fuente
0

Solo si alguien está buscando cómo ocultar modal al hacer clic fuera del modal. Dado que modal generalmente tiene su envoltorio con la clase de modal-wrapo cualquier cosa que le hayas llamado, puedes poner @click="closeModal"el envoltorio. Al usar el manejo de eventos indicado en la documentación de vuejs, puede verificar si el objetivo en el que se hizo clic está en el contenedor o en el modal.

methods: {
  closeModal(e) {
    this.event = function(event) {
      if (event.target.className == 'modal-wrap') {
        // close modal here
        this.$store.commit("catalog/hideModal");
        document.body.removeEventListener("click", this.event);
      }
    }.bind(this);
    document.body.addEventListener("click", this.event);
  },
}
<div class="modal-wrap" @click="closeModal">
  <div class="modal">
    ...
  </div>
<div>

Jedi
fuente
0

Las soluciones de @Denis Danilenko funcionan para mí, esto es lo que hice: por cierto, estoy usando VueJS CLI3 y NuxtJS aquí y con Bootstrap4, pero también funcionará en VueJS sin NuxtJS:

<div
    class="dropdown ml-auto"
    :class="showDropdown ? null : 'show'">
    <a 
        href="#" 
        class="nav-link" 
        role="button" 
        id="dropdownMenuLink" 
        data-toggle="dropdown" 
        aria-haspopup="true" 
        aria-expanded="false"
        @click="showDropdown = !showDropdown"
        @blur="unfocused">
        <i class="fas fa-bars"></i>
    </a>
    <div 
        class="dropdown-menu dropdown-menu-right" 
        aria-labelledby="dropdownMenuLink"
        :class="showDropdown ? null : 'show'">
        <nuxt-link class="dropdown-item" to="/contact">Contact</nuxt-link>
        <nuxt-link class="dropdown-item" to="/faq">FAQ</nuxt-link>
    </div>
</div>
export default {
    data() {
        return {
            showDropdown: true
        }
    },
    methods: {
    unfocused() {
        this.showDropdown = !this.showDropdown;
    }
  }
}
alfieindesigns
fuente
0

Puede emitir un evento javascript nativo personalizado desde una directiva. Cree una directiva que distribuya un evento desde el nodo, utilizando node.dispatchEvent

let handleOutsideClick;
Vue.directive('out-click', {
    bind (el, binding, vnode) {

        handleOutsideClick = (e) => {
            e.stopPropagation()
            const handler = binding.value

            if (el.contains(e.target)) {
                el.dispatchEvent(new Event('out-click')) <-- HERE
            }
        }

        document.addEventListener('click', handleOutsideClick)
        document.addEventListener('touchstart', handleOutsideClick)
    },
    unbind () {
        document.removeEventListener('click', handleOutsideClick)
        document.removeEventListener('touchstart', handleOutsideClick)
    }
})

Que se puede usar así

h3( v-out-click @click="$emit('show')" @out-click="$emit('hide')" )
Pedro Torchio
fuente
0

Creo un div al final del cuerpo así:

<div v-if="isPopup" class="outside" v-on:click="away()"></div>

Donde .outside es:

.outside {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0px;
  left: 0px;
}

Y away () es un método en la instancia de Vue:

away() {
 this.isPopup = false;
}

Fácil, funciona bien.

Arnaud LiDz
fuente
0

Si tiene un componente con varios elementos dentro del elemento raíz, puede usar esta solución It just works ™ con un booleano.

<template>
  <div @click="clickInside"></div>
<template>
<script>
export default {
  name: "MyComponent",
  methods: {
    clickInside() {
      this.inside = true;
      setTimeout(() => (this.inside = false), 0);
    },
    clickOutside() {
      if (this.inside) return;
      // handle outside state from here
    }
  },
  created() {
    this.__handlerRef__ = this.clickOutside.bind(this);
    document.body.addEventListener("click", this.__handlerRef__);
  },
  destroyed() {
    document.body.removeEventListener("click", this.__handlerRef__);
  },
};
</script>
A1rPun
fuente
0

Utilice este paquete vue-click-outside

Es simple y confiable, utilizado actualmente por muchos otros paquetes. También puede reducir el tamaño de su paquete javascript llamando al paquete solo en los componentes requeridos (vea el ejemplo a continuación).

npm install vue-click-outside

Uso:

<template>
  <div>
    <div v-click-outside="hide" @click="toggle">Toggle</div>
    <div v-show="opened">Popup item</div>
  </div>
</template>

<script>
import ClickOutside from 'vue-click-outside'

export default {
  data () {
    return {
      opened: false
    }
  },

  methods: {
    toggle () {
      this.opened = true
    },

    hide () {
      this.opened = false
    }
  },

  mounted () {
    // prevent click outside event with popupItem.
    this.popupItem = this.$el
  },

  // do not forget this section
  directives: {
    ClickOutside
  }
}
</script>
Smit Patel
fuente
0

No reinvente la rueda, use este paquete v-click-outside

snehanshu.js
fuente
Mira mi respuesta, que sospecho que te gustará más.
Mu-Tsun Tsai
0

Puede crear un nuevo componente que maneje el clic externo

Vue.component('click-outside', {
  created: function () {
    document.body.addEventListener('click', (e) => {
       if (!this.$el.contains(e.target)) {
            this.$emit('clickOutside');
           
        })
  },
  template: `
    <template>
        <div>
            <slot/>
        </div>
    </template>
`
})

Y usa este componente:

<template>
    <click-outside @clickOutside="console.log('Click outside Worked!')">
      <div> Your code...</div>
    </click-outside>
</template>
Dictador47
fuente
-1

Con frecuencia, la gente quiere saber si el usuario deja el componente raíz (funciona con componentes de cualquier nivel)

Vue({
  data: {},
  methods: {
    unfocused : function() {
      alert('good bye');
    }
  }
})
<template>
  <div tabindex="1" @blur="unfocused">Content inside</div>
</template>

Denis Danilenko
fuente
-1

Tengo una solución para manejar el menú desplegable de alternancia:

export default {
data() {
  return {
    dropdownOpen: false,
  }
},
methods: {
      showDropdown() {
        console.log('clicked...')
        this.dropdownOpen = !this.dropdownOpen
        // this will control show or hide the menu
        $(document).one('click.status', (e)=> {
          this.dropdownOpen = false
        })
      },
}
Nicolas S.Xu
fuente
-1

Estoy usando este paquete: https://www.npmjs.com/package/vue-click-outside

Funciona bien para mí

HTML:

<div class="__card-content" v-click-outside="hide" v-if="cardContentVisible">
    <div class="card-header">
        <input class="subject-input" placeholder="Subject" name=""/>
    </div>
    <div class="card-body">
        <textarea class="conversation-textarea" placeholder="Start a conversation"></textarea>
    </div>
</div>

Mis códigos de script:

import ClickOutside from 'vue-click-outside'
export default
{
    data(){
        return {
            cardContentVisible:false
        }
    },
    created()
    {
    },
    methods:
        {
            openCardContent()
            {
                this.cardContentVisible = true;
            }, hide () {
            this.cardContentVisible = false
                }
        },
    directives: {
            ClickOutside
    }
}
Murad Shukurlu
fuente