Search code examples
javascriptvuejs3event-handlingvue-component

Vue 3 sub component raises click event when displayed


In vue 3 compose api, I am displaying a popup component and I want to capture out click for closing the modal.

my code in my parent component

<template>
<button @click="toggleSettings" class="bg-gray-300 text-gray-700 px-4 py-2 rounded flex items-center">
<span>⚙️</span>
</button>
<ModalSettings :visible="showSettings" @onClose="toggleSettings"/>
</template>

<script setup lang="ts">
  import { ref } from 'vue'
  const showSettings = ref(false)
const toggleSettings = () => {
  showSettings.value = !showSettings.value
}
</script>

The code of my child component

<template>
    <div  v-if="visible" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
        <div ref="customDiv" @click.stop class="bg-white p-8 rounded-lg shadow-lg w-96">
            <h2 class="text-xl font-bold mb-4"> Settings</h2>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, defineProps } from 'vue'

const customDiv = ref<HTMLElement | null>(null)

const props = defineProps({
    visible: Boolean
})

onMounted(() => {
  nextTick(() => {  
    setTimeout(() => {
      document.addEventListener('click', handleClickOutside)
    }, 1000)
  })
})

onUnmounted(() => {
  document.removeEventListener("click", handleClickOutside)
})

const handleClickOutside = (event: MouseEvent) => {
  if (customDiv.value && !customDiv.value.contains(event.target)) {
    console.log('close the popup')
  }
}
</script>

The issue is each time I display the modal component, it is as if a click is immediately (event with my timeout hack) raised outside of the modal component and so it closes the component (here in an example, it just writes "close the popup")

Do you know what happens and how to prevent that?


Solution

  • You mount the component immediately together with the button so your event handler starts acting immediately. The button's click is propagated to the document and then handled by the modal.

    A brute force solution would be to add stop modifier to the button:

    @click.stop="toggleSettings"
    

    Otherwise add/remove listener on visible change (you need setTimeout to skip the button's click and add the listener after it):

    See on Vue SFC Playground

    watch(() => props.visible, value => 
        setTimeout(() => document[`${value ? 'add' : 'remove'}EventListener`]('click', handleClickOutside)));
    

    P.S. Oops, don't remove, the handler should be removed on the unmounting also

    onUnmounted(() => {
      document.removeEventListener("click", handleClickOutside)
    })