Detectar clique fora do elemento

121

Como posso detectar um clique fora do meu elemento? Estou usando o Vue.js, então ele estará fora do meu elemento de templates. Eu sei fazer no Vanilla JS, mas não tenho certeza se existe uma maneira mais adequada de fazer isso, quando estou usando o Vue.js?

Esta é a solução para Vanilla JS: Javascript Detectar evento de clique fora de div

Acho que posso usar uma maneira melhor de acessar o elemento?

Comunidade
fonte
Os componentes do Vue são isolados. portanto, detectar mudanças externas está fora de questão e o anti-padrão é usado.
Raj Kamal
Obrigado. Não tenho certeza de como implementá-lo em um componente Vue. Ainda deve haver algumas práticas recomendadas para o antipadrão?
Os componentes Vue.js são isolados, isso é verdade, mas existem métodos diferentes para comunicação pai-filho. Assim, em vez de pedir para detectar um evento fora de um elemento, você deve especificar se deseja detectar os elementos dentro de um componente, do componente pai, de algum filho, ou qualquer relação entre os componentes
Yerko Palma
Obrigado pelo feedback. Você tem alguns exemplos ou links que eu possa acompanhar?
github.com/simplesmiler/vue-clickaway pode simplificar seu trabalho
Raj Kamal

Respostas:

97

Pode ser resolvido muito bem configurando uma diretiva personalizada uma 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>

No componente:

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

Demonstração de trabalho no JSFiddle com informações adicionais sobre advertências:

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

Linus Borg
fonte
3
Eu usei o vue clickaway, mas acho que sua solução é mais ou menos a mesma. Obrigado.
56
Essa abordagem não funciona mais no Vue.js 2. A chamada self.vm. $ emit fornece uma mensagem de erro.
norte
3
Usar @blur também é uma opção e torna mais fácil dar o mesmo resultado: <input @ blur = "hide"> onde hide: function () {this.isActive = false; }
Craws
1
A resposta deve ser editada para afirmar que serve apenas para Vue.js 1
Stéphane Gerber
167

Existe a solução que usei, que é baseada na resposta de Linus Borg e funciona bem com 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)
  },
});

Você se liga a ele usando v-click-outside:

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

Aqui está uma pequena demonstração

Você pode encontrar mais informações sobre as diretivas personalizadas e o que el, binding, vnode significa em https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments

MadisonTrash
fonte
8
Funcionou, mas no Vue 2.0 as diretivas não têm mais uma instância, então isso é indefinido. vuejs.org/v2/guide/migration.html#Custom-Directives-simplified . Não tenho ideia de por que esse violino funciona ou quando essa simplificação foi feita. (Para resolver, substitua "this" por "el" para vincular o evento ao elemento)
Busata
1
Provavelmente funciona porque a janela passou como "this". Eu consertei a resposta. Obrigado por apontar este bug.
MadisonTrash
8
Existe uma maneira de excluir um elemento específico externo? Por exemplo, tenho um botão externo que deve abrir esse elemento e, como aciona os dois métodos, nada acontece.
Žilvinas
5
Você pode explicar vnode.context [binding.expression] (event); ?
Sainath SR
1
como alterar isso para que uma expressão possa ser usada em vez de um método dentro do v-click-out é acionado?
raphadko de
50

Adicione o tabindexatributo ao seu componente para que ele possa ser focado e faça o seguinte:

<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
fonte
4
Uau! Considero esta a solução mais curta e mais limpa. Também o único que funcionou no meu caso.
Matt Komarnicki
3
Só para adicionar a isso, definir um tabindex de -1 fará com que a caixa de destaque pare de aparecer quando você clicar no elemento, mas ainda permitirá que o div seja focalizado.
Colin
1
Por algum motivo, tabindex de -1 não esconde o contorno para mim, então apenas adicionei o outline: none;foco para o elemento.
Art3mix
1
como podemos aplicar isso a um navegador lateral fora da tela que desliza na tela? Não consigo dar foco ao sidenav a menos que seja clicado,
Charles Okwuagwu
1
Esta é a forma mais poderosa com certeza. Obrigado! :)
Canet Robern
23

Existem dois pacotes disponíveis na comunidade para esta tarefa (ambos são mantidos):

Julien Le Coupanec
fonte
8
vue-clickawaypacote resolveu meu problema perfeitamente. Obrigado
Abdalla Arbab
1
Que tal muitos itens? Cada item com evento de clique externo irá disparar evento a cada clique. É bom quando você cria diálogos e terrível quando cria uma galeria. Na era dos não componentes, estamos ouvindo cliques do documento e verificando qual elemento foi clicado. Mas agora é uma dor.
br.
@Julien Le Coupanec Eu achei esta solução a melhor! Muito obrigado por compartilhá-lo!
Manuel Abascal
7

Isso funcionou para mim com 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
fonte
Obrigado por este exemplo. Verifiquei isso na vue 2.6. Há alguma correção, no método unbind você deve corrigir algum problema com isso (você esqueceu a propriedade body no método unbind): document.body.removeEventListener ('click', el .__ vueClickOutside__); caso contrário, causará a criação de vários ouvintes de evento após cada recriação do componente (atualização da 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
fonte
5

Combinei todas as respostas (incluindo uma linha de vue-clickaway) e encontrei esta solução que funciona para mim:

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

Use no componente:

<li v-click-outside="closeSearch">
  <!-- your component here -->
</li>
BogdanG
fonte
Praticamente igual à resposta de @MadisonTrash abaixo
retrovertigo
3

Eu atualizei a resposta do MadisonTrash para oferecer suporte ao Mobile Safari (que não tem clickevento, touchenddeve ser usado no lugar). Isso também incorpora uma verificação para que o evento não seja acionado arrastando em dispositivos móveis.

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
fonte
3

Eu uso este código:

botão mostrar-ocultar

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

elemento mostrar-ocultar

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

roteiro

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

Atualizar: remover relógio; adicionar parar propagação

Pax Exterminatus
fonte
2

Eu odeio funções adicionais, então ... aqui está uma solução incrível vue sem métodos adicionais de vue, apenas var

  1. criar elemento html, definir controles e diretiva
    <p @click="popup = !popup" v-out="popup">

    <div v-if="popup">
       My awesome popup
    </div>
  1. criar uma var em dados como
data:{
   popup: false,
}
  1. adicionar diretiva vue. Está
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
fonte
2

Se estiver procurando especificamente por um clique fora do elemento, mas ainda dentro do pai, você pode usar

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

Eu uso isso para modais.

Andres Holguin
fonte
1

Você pode registrar dois ouvintes de evento para um evento de clique 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
fonte
Obrigado. Eu sei disso, mas parece que deve haver uma maneira melhor de fazer isso no Vue.js?
ESTÁ BEM! deixe algum gênio do vue.js responder :)
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
fonte
Obrigado, funcionando perfeitamente e se você só precisar uma vez, não há necessidade de bibliotecas extras
Marian Klühspies
1

A resposta curta: Isso deve ser feito com Diretivas Personalizadas .

Há muitas respostas excelentes aqui que também dizem isso, mas a maioria das respostas que vi se quebram quando você começa a usar o clique externo extensivamente (especialmente em camadas ou com várias exclusões). Eu escrevi um artigo sobre o meio falando sobre as nuances das Diretivas Personalizadas e, especificamente, a implementação desta. Pode não abranger todos os casos extremos, mas cobriu tudo que eu pensei.

Isso levará em conta várias ligações, vários níveis de outras exclusões de elementos e permitirá que seu manipulador gerencie apenas a "lógica de negócios".

Aqui está o código para pelo menos a parte de definição dele, verifique o artigo para uma explicação 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
fonte
1

Fiz isso de uma maneira um pouco diferente usando uma função em created ().

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

Dessa forma, se alguém clicar fora do elemento, no meu caso, o navegador móvel ficará oculto.

Espero que isto ajude!

Quase Pitt
fonte
1

Já existem muitas respostas para essa pergunta, e a maioria delas é baseada na ideia de diretiva personalizada semelhante. O problema com essa abordagem é que é necessário passar uma função de método para a diretiva e não pode escrever código diretamente como em outros eventos.

Criei um novo pacote vue-on-clickoutdiferente. Confira em:

Permite escrever v-on:clickoutcomo qualquer outro evento. Por exemplo, você pode escrever

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

e funciona.

Atualizar

vue-on-clickout agora suporta Vue 3!

Mu-Tsun Tsai
fonte
0

Apenas se alguém estiver procurando como ocultar o modal ao clicar fora do modal. Como modal geralmente tem seu invólucro com a classe de modal-wrapou qualquer coisa que você tenha nomeado, você pode colocar @click="closeModal"o invólucro. Usando o tratamento de eventos declarado na documentação do vuejs, você pode verificar se o destino clicado está no wrapper ou no 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
fonte
0

As soluções @Denis Danilenko funcionam para mim, eis o que fiz: A propósito, estou usando o VueJS CLI3 e o NuxtJS aqui e com o Bootstrap4, mas também funcionará no VueJS sem o 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
fonte
0

Você pode emitir um evento javascript nativo personalizado a partir de uma diretiva. Crie uma diretiva que despacha um evento do nó, usando 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 pode ser usado assim

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

Eu crio um div no final do corpo assim:

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

Onde .fora está:

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

E away () é um método na instância Vue:

away() {
 this.isPopup = false;
}

Fácil, funciona bem.

Arnaud LiDz
fonte
0

Se você tiver um componente com vários elementos dentro do elemento raiz, poderá usar esta solução It just works ™ com um 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
fonte
0

Use este pacote vue-click-outside

É simples e confiável, atualmente usado por muitos outros pacotes. Você também pode reduzir o tamanho do pacote javascript chamando o pacote apenas nos componentes necessários (veja o exemplo abaixo).

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
fonte
0

Não reinvente a roda, use este pacote v-click-outside

snehanshu.js
fonte
Verifique minha resposta, da qual suspeito que você gostará mais.
Mu-Tsun Tsai
0

Você pode criar um novo componente que manipula o clique 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>
`
})

E use este componente:

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

frequentemente as pessoas querem saber se o usuário deixa o componente raiz (funciona com qualquer componente de nível)

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

Denis Danilenko
fonte
-1

Eu tenho uma solução para lidar com o menu suspenso de alternância:

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
fonte
-1

Estou usando este pacote: https://www.npmjs.com/package/vue-click-outside

Isso funciona bem para mim

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>

Meus 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
fonte