How to add custom events for native elements in Vue3

Posted May 26, 20203 min read

Foreword

Recently, vue3 released the beta version, and there is more and more research on its new features. I am also thinking about how to migrate some vue2 tools to vue3. Just a few days ago I thought about how to use the clickoutside event for an element in vue3. I just took this opportunity to record it briefly.

Start up

I want to be able to use the "@event" notation of vue to trigger the event. The component looks like this

<template>
  <div @ clickoutside = "handleClickOutside"> </div>
</template>

<script>
export default {
  name:'MyComponent',
  setup() {
    function handleClickOutside() {
      //some action
    }

    return {
      handleClickOutside
    }
  }
}
<script>

The basic idea is to let the event be dispatched directly from the element, so that vue can catch the event and make a callback

So the first thing to achieve is the logical implementation of clickoutside under native js. I believe everyone here understands the implementation, so I wo n’t go into details, just go to the code ~

First, the distribution part of the event

//Used to store elements that need clickoutside
const elementSet = new Set()

document.addEventListener('click', event => {
  const target = event.target
  const path = event.path ||(event.composedPath && event.composedPath())

  elementSet.forEach(el => {
    if(target! == el &&(path?! path.includes(el):! el.contains(target))) {
      dispatchEvent(el, {type:'clickoutside'})
    }
  })
})

function dispatchEvent(el, payload = {}) {
  const {type, bubbles = false, cancelable = false, ... data} = payload
  const event = new Event(type, {bubbles, cancelable})

  Object.assign(event, data)

  return el.dispatchEvent(event)
}

Then is how to put the root element of the component into set. The method of getting the root element in vue3 is a little different from the original

//vue2
export default {
  mounted() {
    console.log(this. $el)
  }
}

//vue3
import {onMounted, getCurrentInstance} from 'vue'

export default {
  setup() {
    onMounted(() => {
      const instance = getCurrentInstance()

      console.log(instance.ctx. $el)
    })
  }
}

So, you only need to add or remove the root element node at the appropriate time, and you are done

import {onMounted, onBeforeUnmount, getCurrentInstance} from 'vue'

function useClickOutside() {
  const instance = getCurrentInstance()

  onMounted(() => {
    elementSet.add(instance.ctx. $el)
  })

  onBeforeUnmount(() => {
    elementSet.delete(instance.ctx. $el)
  })
}

Slightly improved, can support the binding of any element

import {ref, onMounted, onBeforeUnmount} from 'vue'

function useClickOutside() {
  const wrapper = ref(null)

  onMounted(() => {
    elementSet.add(wrapper)
  })

  onBeforeUnmount(() => {
    elementSet.delete(wrapper)
  })

  return wrapper
}

<template>
  <div ref = "wrapper" @ clickoutside = "handleClickOutside"> </div>
</template>

<script>
import {useClickOutside} from 'clickoutside'

export default {
  name:'MyComponent',
  setup() {
    const wrapper = useClickOutside()

    function handleClickOutside() {
      //some action
    }

    return {
      wrapper,
      handleClickOutside
    }
  }
}
</script>

At last

In fact, as long as the custom event distribution logic is written, any custom event can be called in the form of @ event in vue

The complete code is posted directly

import {ref, onMounted, onBeforeUnmount, nextTick} from 'vue'

const isNull = any => typeof any === 'undefined' || any === null

const USE_TOUCH = isNull(window) &&('ontouchstart' in window ||(isNull(navigator) && navigator.msMaxTouchPoints> 0))
const CLICK_TYPE = USE_TOUCH? 'touchstart':'click'

const CLICK_OUTSIDE = 'clickoutside'

const events = {
  [CLICK_OUTSIDE]:new Set()
}

document.addEventListener(CLICK_TYPE, event => {
  const target = event.target
  const type = CLICK_OUTSIDE
  const path = event.path ||(event.composedPath && event.composedPath())

  events [type].forEach(el => {
    if(target! == el &&(path?! path.includes(el):! el.contains(target))) {
      dispatchEvent(el, {type})
    }
  })
})

function observe(el, types) {
  if(typeof types === 'string') {
    types = [types]
  }

  Array.isArray(types) && types.forEach(type => {
    events [type].add(el)
  })
}

function disconnect(el, types) {
  if(typeof types === 'string') {
    types = [types]
  }

  Array.isArray(types) && types.forEach(type => {
    events [type].delete(el)
  })
}

function dispatchEvent(el, payload = {}, Event = window.Event) {
  const {type, bubbles = false, cancelable = false, ... data} = payload

  let event

  if(! isNull(Event)) {
    event = new Event(type, {bubbles, cancelable})
  } else {
    event = document.createEvent('HTMLEvents')
    event.initEvent(type, bubbles, cancelable)
  }

  Object.assign(event, data)

  return el.dispatchEvent(event)
}

export function useClickOutside() {
  const wrapper = ref(null)

  onMounted(() => {
    nextTick(() => {
      observe(wrapper.value, CLICK_OUTSIDE)
    })
  })

  onBeforeUnmount(() => {
    disconnect(wrapper.value, CLICK_OUTSIDE)
  })

  return wrapper
}